Refactor UI imports and update component paths
Some checks failed
Build Frontend / build (push) Failing after 7s
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:
@@ -4,7 +4,7 @@
|
||||
let toast: any;
|
||||
try {
|
||||
// Try to import from the UI components
|
||||
toast = require("@/components/ui/use-toast").toast;
|
||||
toast = require("@/components/common/use-toast").toast;
|
||||
} catch (error) {
|
||||
// Fallback toast function if not available
|
||||
toast = {
|
||||
@@ -512,4 +512,4 @@ export async function clientFetchWithCache<T = any>(
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,7 @@ export {
|
||||
type Product,
|
||||
type ProductsResponse,
|
||||
type StockData,
|
||||
} from './services/product-service';
|
||||
} from '../services/product-service';
|
||||
|
||||
// Re-export shipping services
|
||||
export {
|
||||
@@ -46,7 +46,7 @@ export {
|
||||
// Types
|
||||
type ShippingOption,
|
||||
type ShippingOptionsResponse,
|
||||
} from './services/shipping-service';
|
||||
} from '../services/shipping-service';
|
||||
|
||||
// Re-export analytics services
|
||||
export {
|
||||
@@ -61,15 +61,15 @@ export {
|
||||
getCustomerInsightsWithStore,
|
||||
getOrderAnalyticsWithStore,
|
||||
getStoreIdForUser,
|
||||
formatGBP,
|
||||
|
||||
// Types
|
||||
type AnalyticsOverview,
|
||||
type RevenueData,
|
||||
type ProductPerformance,
|
||||
type CustomerInsights,
|
||||
type OrderAnalytics,
|
||||
} from './services/analytics-service';
|
||||
} from '../services/analytics-service';
|
||||
|
||||
export { formatGBP, formatNumber } from '../utils/format';
|
||||
|
||||
// Define the PlatformStats interface to match the expected format
|
||||
export interface PlatformStats {
|
||||
@@ -89,7 +89,7 @@ export interface PlatformStats {
|
||||
export {
|
||||
getPlatformStats,
|
||||
getVendorStats,
|
||||
} from './services/stats-service';
|
||||
} from '../services/stats-service';
|
||||
|
||||
// Re-export server API functions
|
||||
export {
|
||||
@@ -111,8 +111,8 @@ export {
|
||||
|
||||
// Get clientFetch first so we can use it in the compatibility functions
|
||||
import { clientFetch } from './api-client';
|
||||
import { getProductDetails, updateProduct } from './services/product-service';
|
||||
import { getPlatformStats } from './services/stats-service';
|
||||
import { getProductDetails, updateProduct } from '../services/product-service';
|
||||
import { getPlatformStats } from '../services/stats-service';
|
||||
|
||||
// Add missing functions for backward compatibility
|
||||
// These are functions from the old style that we need to maintain compatibility
|
||||
@@ -22,20 +22,20 @@ try {
|
||||
*/
|
||||
function getServerApiUrl(endpoint: string): string {
|
||||
const apiUrl = process.env.SERVER_API_URL || 'http://localhost:3001/api';
|
||||
|
||||
|
||||
// Validate API URL to prevent 500 errors
|
||||
if (!apiUrl || apiUrl === 'undefined' || apiUrl === 'null') {
|
||||
console.warn('SERVER_API_URL not properly set, using localhost fallback');
|
||||
const fallbackUrl = 'http://localhost:3001/api';
|
||||
const cleanEndpoint = endpoint.startsWith('/') ? endpoint.substring(1) : endpoint;
|
||||
return fallbackUrl.endsWith('/')
|
||||
return fallbackUrl.endsWith('/')
|
||||
? `${fallbackUrl}${cleanEndpoint}`
|
||||
: `${fallbackUrl}/${cleanEndpoint}`;
|
||||
}
|
||||
|
||||
|
||||
const cleanEndpoint = endpoint.startsWith('/') ? endpoint.substring(1) : endpoint;
|
||||
|
||||
return apiUrl.endsWith('/')
|
||||
|
||||
return apiUrl.endsWith('/')
|
||||
? `${apiUrl}${cleanEndpoint}`
|
||||
: `${apiUrl}/${cleanEndpoint}`;
|
||||
}
|
||||
@@ -58,18 +58,18 @@ export async function fetchServer<T = unknown>(
|
||||
"For client components, use clientFetch or fetchClient instead."
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// Get auth token from cookies
|
||||
const cookieStore = await cookiesModule.cookies();
|
||||
const authToken = cookieStore.get('Authorization')?.value;
|
||||
|
||||
// Redirect to login if not authenticated
|
||||
if (!authToken) redirect('/login');
|
||||
|
||||
if (!authToken) redirect('/auth/login');
|
||||
|
||||
try {
|
||||
// Get the complete backend URL using the utility function
|
||||
const url = getServerApiUrl(endpoint);
|
||||
|
||||
|
||||
// Make the request with proper auth headers
|
||||
const res = await fetch(url, {
|
||||
...options,
|
||||
@@ -82,8 +82,8 @@ export async function fetchServer<T = unknown>(
|
||||
});
|
||||
|
||||
// Handle auth failures
|
||||
if (res.status === 401) redirect('/login');
|
||||
|
||||
if (res.status === 401) redirect('/auth/login');
|
||||
|
||||
// Handle other errors
|
||||
if (!res.ok) {
|
||||
let errorData;
|
||||
@@ -92,7 +92,7 @@ export async function fetchServer<T = unknown>(
|
||||
} catch {
|
||||
errorData = {};
|
||||
}
|
||||
|
||||
|
||||
// Handle new error format: { success: false, error: { message: "...", code: "..." } }
|
||||
// or old format: { error: "...", message: "..." }
|
||||
let errorMessage: string;
|
||||
@@ -105,7 +105,7 @@ export async function fetchServer<T = unknown>(
|
||||
} else {
|
||||
errorMessage = `Request failed: ${res.status} ${res.statusText}`;
|
||||
}
|
||||
|
||||
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
@@ -158,22 +158,22 @@ export const getCustomerDetailsServer = async (userId: string): Promise<Customer
|
||||
export async function getPlatformStatsServer() {
|
||||
try {
|
||||
const url = getServerApiUrl('/stats/platform');
|
||||
|
||||
|
||||
// Make direct request without auth
|
||||
const res = await fetch(url, {
|
||||
cache: 'no-store',
|
||||
cache: 'no-store',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
|
||||
// If we have real data, use it
|
||||
if (data && Object.keys(data).length > 0) {
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
console.info('Using sample stats data for demo');
|
||||
return {
|
||||
orders: {
|
||||
@@ -296,7 +296,7 @@ export const getAnalyticsOverviewServer = async (storeId?: string): Promise<Anal
|
||||
export const getRevenueTrendsServer = async (period: string = '30', storeId?: string): Promise<RevenueData[]> => {
|
||||
const params = new URLSearchParams({ period });
|
||||
if (storeId) params.append('storeId', storeId);
|
||||
|
||||
|
||||
const url = `/analytics/revenue-trends?${params.toString()}`;
|
||||
return fetchServer<RevenueData[]>(url);
|
||||
};
|
||||
@@ -314,7 +314,7 @@ export const getCustomerInsightsServer = async (storeId?: string): Promise<Custo
|
||||
export const getOrderAnalyticsServer = async (period: string = '30', storeId?: string): Promise<OrderAnalytics> => {
|
||||
const params = new URLSearchParams({ period });
|
||||
if (storeId) params.append('storeId', storeId);
|
||||
|
||||
|
||||
const url = `/analytics/order-analytics?${params.toString()}`;
|
||||
return fetchServer<OrderAnalytics>(url);
|
||||
};
|
||||
160
lib/hooks/use-chromebook-keyboard.tsx
Normal file
160
lib/hooks/use-chromebook-keyboard.tsx
Normal 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
|
||||
};
|
||||
}
|
||||
86
lib/hooks/use-chromebook-scroll.tsx
Normal file
86
lib/hooks/use-chromebook-scroll.tsx
Normal 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
52
lib/hooks/use-mobile.tsx
Normal 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
194
lib/hooks/use-toast.ts
Normal 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
165
lib/hooks/useFilterState.ts
Normal 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
|
||||
}
|
||||
}
|
||||
57
lib/hooks/useKeepOnline.ts
Normal file
57
lib/hooks/useKeepOnline.ts
Normal 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
50
lib/hooks/useUser.ts
Normal 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
|
||||
}
|
||||
}
|
||||
138
lib/hooks/useWidgetLayout.ts
Normal file
138
lib/hooks/useWidgetLayout.ts
Normal 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
|
||||
}
|
||||
}
|
||||
6
lib/models/categories.ts
Normal file
6
lib/models/categories.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export interface Category {
|
||||
_id: string;
|
||||
name: string;
|
||||
parentId?: string;
|
||||
order?: number;
|
||||
}
|
||||
19
lib/models/products.ts
Normal file
19
lib/models/products.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
export interface Product {
|
||||
_id?: string;
|
||||
name: string;
|
||||
description: string;
|
||||
unitType: "pcs" | "gr" | "kg" | "ml" | "oz" | "lb";
|
||||
category: string;
|
||||
enabled?: boolean;
|
||||
// Stock management fields
|
||||
stockTracking?: boolean;
|
||||
currentStock?: number;
|
||||
lowStockThreshold?: number;
|
||||
stockStatus?: "in_stock" | "low_stock" | "out_of_stock";
|
||||
pricing: Array<{
|
||||
minQuantity: number;
|
||||
pricePerUnit: number;
|
||||
}>;
|
||||
image?: string | File | null | undefined;
|
||||
costPerUnit?: number;
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import React, { createContext, useContext, useState, useEffect, useRef, ReactNod
|
||||
import { clientFetch } from "@/lib/api";
|
||||
import { toast } from "sonner";
|
||||
import { getCookie } from "@/lib/api";
|
||||
import { cacheUtils } from '@/lib/api-client';
|
||||
import { cacheUtils } from '@/lib/api/api-client';
|
||||
import { Package } from "lucide-react";
|
||||
|
||||
interface Order {
|
||||
@@ -343,4 +343,4 @@ export function useNotifications() {
|
||||
throw new Error('useNotifications must be used within a NotificationProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { clientFetch } from "../api-client";
|
||||
import { clientFetch } from '@/lib/api/api-client';
|
||||
import { formatGBP } from '@/lib/utils/format';
|
||||
|
||||
// Analytics Types
|
||||
export interface AnalyticsOverview {
|
||||
@@ -293,15 +294,6 @@ export const getGrowthAnalyticsWithStore =
|
||||
return getGrowthAnalytics(storeId);
|
||||
};
|
||||
|
||||
export function formatGBP(value: number) {
|
||||
return value.toLocaleString("en-GB", {
|
||||
style: "currency",
|
||||
currency: "GBP",
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
});
|
||||
}
|
||||
|
||||
// Prediction Types
|
||||
export interface SalesPrediction {
|
||||
predicted: number | null;
|
||||
|
||||
8
lib/services/customerService.ts
Normal file
8
lib/services/customerService.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
// This file is maintained for backward compatibility
|
||||
// Import from the new consolidated API
|
||||
export {
|
||||
getCustomers,
|
||||
getCustomerDetails,
|
||||
type CustomerStats,
|
||||
type CustomerResponse
|
||||
} from '../lib/api';
|
||||
@@ -1,5 +1,3 @@
|
||||
// Re-export all service functionality
|
||||
export * from './product-service';
|
||||
export * from './shipping-service';
|
||||
export * from './stats-service';
|
||||
export * from '../api-client';
|
||||
// Re-export all services from the lib directory
|
||||
// This provides backward compatibility
|
||||
export * from '../lib/api';
|
||||
@@ -1,4 +1,4 @@
|
||||
import { clientFetch } from '../api-client';
|
||||
import { clientFetch } from '@/lib/api/api-client';
|
||||
|
||||
// Product data types
|
||||
export interface Product {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { clientFetch } from '../api-client';
|
||||
import { clientFetch } from '@/lib/api/api-client';
|
||||
|
||||
/**
|
||||
* Shipping service - Handles shipping options
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { clientFetch } from '../api-client';
|
||||
import { clientFetch } from '@/lib/api/api-client';
|
||||
|
||||
// Stats data types
|
||||
export interface PlatformStats {
|
||||
|
||||
61
lib/types/dashboard.ts
Normal file
61
lib/types/dashboard.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
export interface RecentActivitySettings {
|
||||
itemCount: number // 5, 10, 15
|
||||
}
|
||||
|
||||
export interface TopProductsSettings {
|
||||
itemCount: number // 3, 5, 10
|
||||
showRevenue: boolean
|
||||
}
|
||||
|
||||
export interface OverviewSettings {
|
||||
showChange: boolean // Show % change from previous period
|
||||
}
|
||||
|
||||
export interface RevenueChartSettings {
|
||||
days: number // 7, 14, 30
|
||||
showComparison: boolean
|
||||
}
|
||||
|
||||
export interface LowStockSettings {
|
||||
threshold: number // Show items with stock below this
|
||||
itemCount: number
|
||||
}
|
||||
|
||||
export interface RecentCustomersSettings {
|
||||
itemCount: number
|
||||
showSpent: boolean
|
||||
}
|
||||
|
||||
export interface PendingChatsSettings {
|
||||
showPreview: boolean
|
||||
}
|
||||
|
||||
export type WidgetSettings =
|
||||
| { type: "quick-actions" }
|
||||
| { type: "overview"; settings: OverviewSettings }
|
||||
| { type: "recent-activity"; settings: RecentActivitySettings }
|
||||
| { type: "top-products"; settings: TopProductsSettings }
|
||||
| { type: "revenue-chart"; settings: RevenueChartSettings }
|
||||
| { type: "low-stock"; settings: LowStockSettings }
|
||||
| { type: "recent-customers"; settings: RecentCustomersSettings }
|
||||
| { type: "pending-chats"; settings: PendingChatsSettings }
|
||||
|
||||
export interface WidgetConfig {
|
||||
id: string
|
||||
title: string
|
||||
visible: boolean
|
||||
order: number
|
||||
colSpan?: number
|
||||
settings?: Record<string, any>
|
||||
}
|
||||
|
||||
export interface TopProduct {
|
||||
id: string;
|
||||
name: string;
|
||||
price: number | number[];
|
||||
image: string;
|
||||
count: number;
|
||||
revenue: number;
|
||||
unitType?: string;
|
||||
currentStock?: number;
|
||||
}
|
||||
28
lib/utils/format.ts
Normal file
28
lib/utils/format.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
export const formatCurrency = (amount: number | undefined | null): string => {
|
||||
if (amount === undefined || amount === null || isNaN(Number(amount))) {
|
||||
return '£0.00';
|
||||
}
|
||||
return new Intl.NumberFormat('en-GB', {
|
||||
style: 'currency',
|
||||
currency: 'GBP'
|
||||
}).format(Number(amount));
|
||||
};
|
||||
|
||||
export function formatGBP(value: number | undefined | null) {
|
||||
if (value === undefined || value === null || isNaN(Number(value))) {
|
||||
return '£0.00';
|
||||
}
|
||||
return Number(value).toLocaleString('en-GB', {
|
||||
style: 'currency',
|
||||
currency: 'GBP',
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
});
|
||||
}
|
||||
|
||||
export function formatNumber(value: number | undefined | null, options: Intl.NumberFormatOptions = {}) {
|
||||
if (value === undefined || value === null || isNaN(Number(value))) {
|
||||
return '0';
|
||||
}
|
||||
return Number(value).toLocaleString('en-GB', options);
|
||||
}
|
||||
Reference in New Issue
Block a user