Refactor UI imports and update component paths
Some checks failed
Build Frontend / build (push) Failing after 7s

Replaces imports from 'components/ui' with 'components/common' across the app and dashboard pages, and updates model and API imports to use new paths under 'lib'. Removes redundant authentication checks from several dashboard pages. Adds new dashboard components and utility files, and reorganizes hooks and services into the 'lib' directory for improved structure.
This commit is contained in:
g
2026-01-13 05:02:13 +00:00
parent a6e6cd0757
commit fe01f31538
173 changed files with 1512 additions and 867 deletions

View File

@@ -0,0 +1,160 @@
import { useEffect, useCallback } from 'react';
/**
* Hook for enhanced keyboard navigation on Chromebooks
*/
export function useChromebookKeyboard() {
const handleKeyDown = useCallback((e: KeyboardEvent) => {
// Enhanced keyboard shortcuts for Chromebooks
const { key, ctrlKey, metaKey, altKey, shiftKey } = e;
// Chromebook-specific shortcuts
if (metaKey || ctrlKey) {
switch (key) {
case 'k':
// Focus search or command palette
e.preventDefault();
const searchInput = document.querySelector('input[type="search"], input[placeholder*="search" i]') as HTMLInputElement;
if (searchInput) {
searchInput.focus();
searchInput.select();
}
break;
case 'Enter':
// Submit forms with Ctrl/Cmd + Enter
e.preventDefault();
const form = document.querySelector('form') as HTMLFormElement;
if (form) {
const submitButton = form.querySelector('button[type="submit"]') as HTMLButtonElement;
if (submitButton && !submitButton.disabled) {
submitButton.click();
}
}
break;
case 'ArrowUp':
case 'ArrowDown':
// Navigate through messages or list items
e.preventDefault();
const focusableElements = document.querySelectorAll(
'button, input, textarea, [tabindex]:not([tabindex="-1"]), [role="button"], [role="tab"]'
) as NodeListOf<HTMLElement>;
const currentIndex = Array.from(focusableElements).indexOf(document.activeElement as HTMLElement);
let nextIndex;
if (key === 'ArrowUp') {
nextIndex = currentIndex > 0 ? currentIndex - 1 : focusableElements.length - 1;
} else {
nextIndex = currentIndex < focusableElements.length - 1 ? currentIndex + 1 : 0;
}
focusableElements[nextIndex]?.focus();
break;
}
}
// Escape key handling
if (key === 'Escape') {
// Close modals, clear inputs, or go back
const activeElement = document.activeElement as HTMLElement;
if (activeElement?.tagName === 'INPUT' || activeElement?.tagName === 'TEXTAREA') {
// Clear input on Escape
(activeElement as HTMLInputElement).value = '';
activeElement.blur();
} else {
// Look for close buttons or back buttons
const closeButton = document.querySelector('[aria-label*="close" i], [aria-label*="back" i]') as HTMLElement;
if (closeButton) {
closeButton.click();
}
}
}
// Tab navigation enhancement
if (key === 'Tab') {
// Ensure proper tab order for Chromebooks
const focusableElements = document.querySelectorAll(
'button:not([disabled]), input:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"]), [role="button"]:not([disabled]), [role="tab"]'
);
// Add visual focus indicators
const addFocusIndicator = (element: Element) => {
element.classList.add('keyboard-focus');
};
const removeFocusIndicator = (element: Element) => {
element.classList.remove('keyboard-focus');
};
// Handle focus events
focusableElements.forEach(element => {
element.addEventListener('focus', () => addFocusIndicator(element));
element.addEventListener('blur', () => removeFocusIndicator(element));
});
}
}, []);
useEffect(() => {
// Add global keyboard event listener
document.addEventListener('keydown', handleKeyDown);
// Cleanup
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [handleKeyDown]);
return {
handleKeyDown
};
}
/**
* Hook for managing focus in chat interfaces
*/
export function useChatFocus() {
const focusMessageInput = useCallback(() => {
const messageInput = document.querySelector('input[aria-label*="message" i], textarea[aria-label*="message" i]') as HTMLInputElement;
if (messageInput) {
messageInput.focus();
messageInput.select();
}
}, []);
const focusNextMessage = useCallback(() => {
const messages = document.querySelectorAll('[role="article"]');
const currentMessage = document.activeElement?.closest('[role="article"]');
if (currentMessage) {
const currentIndex = Array.from(messages).indexOf(currentMessage);
const nextMessage = messages[currentIndex + 1] as HTMLElement;
if (nextMessage) {
nextMessage.focus();
}
} else if (messages.length > 0) {
(messages[0] as HTMLElement).focus();
}
}, []);
const focusPreviousMessage = useCallback(() => {
const messages = document.querySelectorAll('[role="article"]');
const currentMessage = document.activeElement?.closest('[role="article"]');
if (currentMessage) {
const currentIndex = Array.from(messages).indexOf(currentMessage);
const previousMessage = messages[currentIndex - 1] as HTMLElement;
if (previousMessage) {
previousMessage.focus();
}
}
}, []);
return {
focusMessageInput,
focusNextMessage,
focusPreviousMessage
};
}

View File

@@ -0,0 +1,86 @@
import { useEffect, useRef } from 'react';
/**
* Hook to enhance scrolling behavior for Chromebooks and touch devices
*/
export function useChromebookScroll() {
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const container = containerRef.current;
if (!container) return;
// Enhanced scrolling for Chromebooks
const handleTouchStart = (e: TouchEvent) => {
// Prevent default touch behavior that might interfere with scrolling
if (e.touches.length === 1) {
// Single touch - allow normal scrolling
return;
}
// Multi-touch - prevent zoom gestures
if (e.touches.length > 1) {
e.preventDefault();
}
};
const handleTouchMove = (e: TouchEvent) => {
// Allow momentum scrolling on Chromebooks
if (e.touches.length === 1) {
// Single touch scrolling - allow default behavior
return;
}
// Multi-touch - prevent zoom
if (e.touches.length > 1) {
e.preventDefault();
}
};
const handleWheel = (e: WheelEvent) => {
// Enhanced wheel scrolling for Chromebook trackpads
const delta = e.deltaY;
const container = e.currentTarget as HTMLElement;
// Smooth scrolling for Chromebook trackpads
if (Math.abs(delta) > 0) {
container.scrollBy({
top: delta * 0.5, // Reduce scroll speed for better control
behavior: 'smooth'
});
}
};
// Add event listeners
container.addEventListener('touchstart', handleTouchStart, { passive: false });
container.addEventListener('touchmove', handleTouchMove, { passive: false });
container.addEventListener('wheel', handleWheel, { passive: true });
// Cleanup
return () => {
container.removeEventListener('touchstart', handleTouchStart);
container.removeEventListener('touchmove', handleTouchMove);
container.removeEventListener('wheel', handleWheel);
};
}, []);
return containerRef;
}
/**
* Hook for smooth scrolling to bottom (useful for chat interfaces)
*/
export function useSmoothScrollToBottom() {
const scrollToBottom = (container: HTMLElement) => {
container.scrollTo({
top: container.scrollHeight,
behavior: 'smooth'
});
};
const scrollToBottomInstant = (container: HTMLElement) => {
container.scrollTop = container.scrollHeight;
};
return { scrollToBottom, scrollToBottomInstant };
}

52
lib/hooks/use-mobile.tsx Normal file
View File

@@ -0,0 +1,52 @@
import * as React from "react"
const MOBILE_BREAKPOINT = 768
export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
React.useEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
}
mql.addEventListener("change", onChange)
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
return () => mql.removeEventListener("change", onChange)
}, [])
return !!isMobile
}
export function useIsTouchDevice() {
const [isTouch, setIsTouch] = React.useState<boolean | undefined>(undefined)
React.useEffect(() => {
const checkTouch = () => {
const hasTouch = 'ontouchstart' in window ||
navigator.maxTouchPoints > 0 ||
(navigator.userAgent.includes('CrOS') && 'ontouchstart' in window) ||
window.matchMedia('(pointer: coarse)').matches ||
!window.matchMedia('(hover: hover)').matches
setIsTouch(hasTouch)
}
checkTouch()
const mediaQuery = window.matchMedia('(pointer: coarse)')
const hoverQuery = window.matchMedia('(hover: hover)')
const handleChange = () => checkTouch()
mediaQuery.addEventListener('change', handleChange)
hoverQuery.addEventListener('change', handleChange)
return () => {
mediaQuery.removeEventListener('change', handleChange)
hoverQuery.removeEventListener('change', handleChange)
}
}, [])
return !!isTouch
}

194
lib/hooks/use-toast.ts Normal file
View File

@@ -0,0 +1,194 @@
"use client"
// Inspired by react-hot-toast library
import * as React from "react"
import type {
ToastActionElement,
ToastProps,
} from "@/components/common/toast"
const TOAST_LIMIT = 1
const TOAST_REMOVE_DELAY = 1000000
type ToasterToast = ToastProps & {
id: string
title?: React.ReactNode
description?: React.ReactNode
action?: ToastActionElement
}
const actionTypes = {
ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST",
} as const
let count = 0
function genId() {
count = (count + 1) % Number.MAX_SAFE_INTEGER
return count.toString()
}
type ActionType = typeof actionTypes
type Action =
| {
type: ActionType["ADD_TOAST"]
toast: ToasterToast
}
| {
type: ActionType["UPDATE_TOAST"]
toast: Partial<ToasterToast>
}
| {
type: ActionType["DISMISS_TOAST"]
toastId?: ToasterToast["id"]
}
| {
type: ActionType["REMOVE_TOAST"]
toastId?: ToasterToast["id"]
}
interface State {
toasts: ToasterToast[]
}
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
const addToRemoveQueue = (toastId: string) => {
if (toastTimeouts.has(toastId)) {
return
}
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId)
dispatch({
type: "REMOVE_TOAST",
toastId: toastId,
})
}, TOAST_REMOVE_DELAY)
toastTimeouts.set(toastId, timeout)
}
export const reducer = (state: State, action: Action): State => {
switch (action.type) {
case "ADD_TOAST":
return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
}
case "UPDATE_TOAST":
return {
...state,
toasts: state.toasts.map((t) =>
t.id === action.toast.id ? { ...t, ...action.toast } : t
),
}
case "DISMISS_TOAST": {
const { toastId } = action
// ! Side effects ! - This could be extracted into a dismissToast() action,
// but I'll keep it here for simplicity
if (toastId) {
addToRemoveQueue(toastId)
} else {
state.toasts.forEach((toast) => {
addToRemoveQueue(toast.id)
})
}
return {
...state,
toasts: state.toasts.map((t) =>
t.id === toastId || toastId === undefined
? {
...t,
open: false,
}
: t
),
}
}
case "REMOVE_TOAST":
if (action.toastId === undefined) {
return {
...state,
toasts: [],
}
}
return {
...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId),
}
}
}
const listeners: Array<(state: State) => void> = []
let memoryState: State = { toasts: [] }
function dispatch(action: Action) {
memoryState = reducer(memoryState, action)
listeners.forEach((listener) => {
listener(memoryState)
})
}
type Toast = Omit<ToasterToast, "id">
function toast({ ...props }: Toast) {
const id = genId()
const update = (props: ToasterToast) =>
dispatch({
type: "UPDATE_TOAST",
toast: { ...props, id },
})
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
dispatch({
type: "ADD_TOAST",
toast: {
...props,
id,
open: true,
onOpenChange: (open) => {
if (!open) dismiss()
},
},
})
return {
id: id,
dismiss,
update,
}
}
function useToast() {
const [state, setState] = React.useState<State>(memoryState)
React.useEffect(() => {
listeners.push(setState)
return () => {
const index = listeners.indexOf(setState)
if (index > -1) {
listeners.splice(index, 1)
}
}
}, [state])
return {
...state,
toast,
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
}
}
export { useToast, toast }

165
lib/hooks/useFilterState.ts Normal file
View File

@@ -0,0 +1,165 @@
"use client"
import { useState, useEffect, useCallback } from "react"
import { usePathname, useSearchParams, useRouter } from "next/navigation"
import { DateRange } from "react-day-picker"
interface FilterState {
searchQuery?: string
statusFilter?: string
dateRange?: DateRange
page?: number
itemsPerPage?: number
sortColumn?: string
sortDirection?: "asc" | "desc"
}
interface UseFilterStateOptions {
/** Unique key for this page's filter state */
storageKey: string
/** Initialize from URL params on mount */
syncWithUrl?: boolean
/** Default values */
defaults?: Partial<FilterState>
}
/**
* useFilterState - Persist filter state across navigation
* Uses sessionStorage to remember filters per page
*/
export function useFilterState({
storageKey,
syncWithUrl = false,
defaults = {}
}: UseFilterStateOptions) {
const pathname = usePathname()
const searchParams = useSearchParams()
const router = useRouter()
const fullKey = `filterState:${storageKey}`
// Initialize state from sessionStorage or URL params
const getInitialState = (): FilterState => {
if (typeof window === "undefined") return defaults
// First try URL params if syncWithUrl is enabled
if (syncWithUrl && searchParams) {
const urlState: FilterState = {}
const status = searchParams.get("status")
const search = searchParams.get("search")
const page = searchParams.get("page")
if (status) urlState.statusFilter = status
if (search) urlState.searchQuery = search
if (page) urlState.page = parseInt(page)
if (Object.keys(urlState).length > 0) {
return { ...defaults, ...urlState }
}
}
// Then try sessionStorage
try {
const stored = sessionStorage.getItem(fullKey)
if (stored) {
const parsed = JSON.parse(stored)
// Restore dateRange as Date objects
if (parsed.dateRange) {
if (parsed.dateRange.from) parsed.dateRange.from = new Date(parsed.dateRange.from)
if (parsed.dateRange.to) parsed.dateRange.to = new Date(parsed.dateRange.to)
}
return { ...defaults, ...parsed }
}
} catch (e) {
console.warn("Failed to load filter state from storage:", e)
}
return defaults
}
const [filterState, setFilterState] = useState<FilterState>(getInitialState)
// Save to sessionStorage whenever state changes
useEffect(() => {
if (typeof window === "undefined") return
try {
sessionStorage.setItem(fullKey, JSON.stringify(filterState))
} catch (e) {
console.warn("Failed to save filter state to storage:", e)
}
}, [filterState, fullKey])
// Update URL if syncWithUrl is enabled
useEffect(() => {
if (!syncWithUrl) return
const params = new URLSearchParams()
if (filterState.statusFilter && filterState.statusFilter !== "all") {
params.set("status", filterState.statusFilter)
}
if (filterState.searchQuery) {
params.set("search", filterState.searchQuery)
}
if (filterState.page && filterState.page > 1) {
params.set("page", filterState.page.toString())
}
const queryString = params.toString()
const newUrl = queryString ? `${pathname}?${queryString}` : pathname
// Only update if URL would change
const currentQuery = searchParams?.toString() || ""
if (queryString !== currentQuery) {
router.replace(newUrl, { scroll: false })
}
}, [filterState, syncWithUrl, pathname, router, searchParams])
// Convenience setters
const setSearchQuery = useCallback((query: string) => {
setFilterState(prev => ({ ...prev, searchQuery: query, page: 1 }))
}, [])
const setStatusFilter = useCallback((status: string) => {
setFilterState(prev => ({ ...prev, statusFilter: status, page: 1 }))
}, [])
const setDateRange = useCallback((range: DateRange | undefined) => {
setFilterState(prev => ({ ...prev, dateRange: range, page: 1 }))
}, [])
const setPage = useCallback((page: number) => {
setFilterState(prev => ({ ...prev, page }))
}, [])
const setItemsPerPage = useCallback((count: number) => {
setFilterState(prev => ({ ...prev, itemsPerPage: count, page: 1 }))
}, [])
const setSort = useCallback((column: string, direction: "asc" | "desc") => {
setFilterState(prev => ({ ...prev, sortColumn: column, sortDirection: direction }))
}, [])
const clearFilters = useCallback(() => {
setFilterState(defaults)
}, [defaults])
const hasActiveFilters = Boolean(
filterState.searchQuery ||
(filterState.statusFilter && filterState.statusFilter !== "all") ||
filterState.dateRange?.from
)
return {
...filterState,
setFilterState,
setSearchQuery,
setStatusFilter,
setDateRange,
setPage,
setItemsPerPage,
setSort,
clearFilters,
hasActiveFilters
}
}

View File

@@ -0,0 +1,57 @@
import { useEffect, useRef } from "react";
import { clientFetch } from "@/lib/api";
interface UseKeepOnlineOptions {
interval?: number; // in milliseconds
enabled?: boolean;
onError?: (error: any) => void;
}
export const useKeepOnline = (options: UseKeepOnlineOptions = {}) => {
const {
interval = 1000 * 60 * 1,
enabled = true,
onError
} = options;
const intervalRef = useRef<NodeJS.Timeout | null>(null);
useEffect(() => {
if (!enabled) {
return;
}
const updateOnlineStatus = async () => {
try {
console.log("Updating online status...");
await clientFetch('/auth/me');
} catch (error) {
console.error("Failed to update online status:", error);
onError?.(error);
}
};
// Initial call
updateOnlineStatus();
// Set up interval
intervalRef.current = setInterval(updateOnlineStatus, interval);
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
};
}, [enabled, interval, onError]);
return {
isActive: enabled && intervalRef.current !== null,
stop: () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
}
};
};

50
lib/hooks/useUser.ts Normal file
View File

@@ -0,0 +1,50 @@
"use client"
import { useState, useEffect } from 'react'
import { clientFetch } from '@/lib/api/api-client'
interface Vendor {
_id: string;
username: string;
storeId: string;
pgpKey: string;
__v: number;
}
interface User {
vendor: Vendor;
}
export function useUser() {
const [user, setUser] = useState<User | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
const fetchUser = async () => {
try {
setLoading(true)
const userData = await clientFetch<User>("/auth/me")
setUser(userData)
setError(null)
} catch (err) {
console.error("Failed to fetch user:", err)
setError("Failed to fetch user data")
setUser(null)
} finally {
setLoading(false)
}
}
fetchUser()
}, [])
const isAdmin = user?.vendor?.username === 'admin1'
return {
user,
loading,
error,
isAdmin
}
}

View File

@@ -0,0 +1,138 @@
"use client"
import { useState, useEffect, useCallback } from "react"
import type { WidgetConfig, WidgetSettings } from "@/lib/types/dashboard"
const DEFAULT_WIDGETS: WidgetConfig[] = [
{ id: "quick-actions", title: "Quick Actions", visible: true, order: 0 },
{ id: "overview", title: "Overview", visible: true, order: 1, settings: { showChange: false } },
{ id: "recent-activity", title: "Recent Activity", visible: true, order: 2, settings: { itemCount: 10 } },
{ id: "top-products", title: "Top Products", visible: true, order: 3, settings: { itemCount: 5, showRevenue: true } },
{ id: "revenue-chart", title: "Revenue Chart", visible: false, order: 4, settings: { days: 7, showComparison: false } },
{ id: "low-stock", title: "Low Stock Alerts", visible: false, order: 5, settings: { threshold: 5, itemCount: 5 } },
{ id: "recent-customers", title: "Recent Customers", visible: false, order: 6, settings: { itemCount: 5, showSpent: true } },
{ id: "pending-chats", title: "Pending Chats", visible: false, order: 7, settings: { showPreview: true } },
]
const STORAGE_KEY = "dashboard-widget-layout-v4"
/**
* useWidgetLayout - Persist and manage dashboard widget visibility, order, and settings
*/
export function useWidgetLayout() {
const [widgets, setWidgets] = useState<WidgetConfig[]>(DEFAULT_WIDGETS)
const [isLoaded, setIsLoaded] = useState(false)
// Load from localStorage on mount
useEffect(() => {
if (typeof window === "undefined") return
try {
const stored = localStorage.getItem(STORAGE_KEY)
if (stored) {
const parsed = JSON.parse(stored) as WidgetConfig[]
// Merge with defaults to handle new widgets added in future
const merged = DEFAULT_WIDGETS.map(defaultWidget => {
const savedWidget = parsed.find(w => w.id === defaultWidget.id)
return savedWidget
? { ...defaultWidget, ...savedWidget, settings: { ...defaultWidget.settings, ...savedWidget.settings } }
: defaultWidget
})
setWidgets(merged.sort((a, b) => a.order - b.order))
}
} catch (e) {
console.warn("Failed to load widget layout:", e)
}
setIsLoaded(true)
}, [])
// Save to localStorage whenever widgets change
useEffect(() => {
if (!isLoaded || typeof window === "undefined") return
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(widgets))
} catch (e) {
console.warn("Failed to save widget layout:", e)
}
}, [widgets, isLoaded])
const toggleWidget = useCallback((widgetId: string) => {
setWidgets(prev =>
prev.map(w => w.id === widgetId ? { ...w, visible: !w.visible } : w)
)
}, [])
const moveWidget = useCallback((widgetId: string, direction: "up" | "down") => {
setWidgets(prev => {
const index = prev.findIndex(w => w.id === widgetId)
if (index === -1) return prev
const newIndex = direction === "up" ? index - 1 : index + 1
if (newIndex < 0 || newIndex >= prev.length) return prev
const newWidgets = [...prev]
const [widget] = newWidgets.splice(index, 1)
newWidgets.splice(newIndex, 0, widget)
// Update order values
return newWidgets.map((w, i) => ({ ...w, order: i }))
})
}, [])
const updateWidgetSettings = useCallback((widgetId: string, newSettings: Record<string, any>) => {
setWidgets(prev =>
prev.map(w => w.id === widgetId
? { ...w, settings: { ...w.settings, ...newSettings } }
: w
)
)
}, [])
const getWidgetSettings = useCallback(<T extends Record<string, any>>(widgetId: string): T | undefined => {
return widgets.find(w => w.id === widgetId)?.settings as T | undefined
}, [widgets])
const resetLayout = useCallback(() => {
setWidgets(DEFAULT_WIDGETS)
}, [])
const getVisibleWidgets = useCallback(() => {
return widgets.filter(w => w.visible).sort((a, b) => a.order - b.order)
}, [widgets])
const isWidgetVisible = useCallback((widgetId: string) => {
return widgets.find(w => w.id === widgetId)?.visible ?? true
}, [widgets])
// Reorder widgets by moving activeId to overId's position
const reorderWidgets = useCallback((activeId: string, overId: string) => {
setWidgets(prev => {
const oldIndex = prev.findIndex(w => w.id === activeId)
const newIndex = prev.findIndex(w => w.id === overId)
if (oldIndex === -1 || newIndex === -1) return prev
const newWidgets = [...prev]
const [removed] = newWidgets.splice(oldIndex, 1)
newWidgets.splice(newIndex, 0, removed)
// Update order values
return newWidgets.map((w, i) => ({ ...w, order: i }))
})
}, [])
return {
widgets,
toggleWidget,
moveWidget,
reorderWidgets,
updateWidgetSettings,
getWidgetSettings,
resetLayout,
getVisibleWidgets,
isWidgetVisible,
isLoaded
}
}