From b836392f5cceb6b3947ae5b61e0da808f6dee573 Mon Sep 17 00:00:00 2001 From: Olivier BYIRINGIRO Date: Mon, 1 Dec 2025 15:48:56 +0200 Subject: [PATCH 1/6] improve the sidebar and responsiveness --- app/action/page.tsx | 10 +- app/analytics/page.tsx | 8 +- app/home/transfer/amount/page.tsx | 13 +- app/home/transfer/confirmation/page.tsx | 13 +- app/settings/page.tsx | 8 +- app/statistics/page.tsx | 9 +- app/transactions/page.tsx | 8 +- components/ClientProvider.tsx | 5 +- components/HomePageLayout.tsx | 8 +- components/Navigation.tsx | 60 +- components/chat/real-chat-page.tsx | 8 +- context/SidebarContext.tsx | 50 + package-lock.json | 1212 ++++++++++++++++++++--- 13 files changed, 1228 insertions(+), 184 deletions(-) create mode 100644 context/SidebarContext.tsx diff --git a/app/action/page.tsx b/app/action/page.tsx index 1c92e3f..35b92d0 100644 --- a/app/action/page.tsx +++ b/app/action/page.tsx @@ -1,17 +1,23 @@ - +'use client' import ActionPageLayout from '@/components/ActionPage/ActionPageLayout' import { Header } from '@/components/Header' import Navigation from '@/components/Navigation' import React from 'react' +import { useSidebar } from '@/context/SidebarContext' +import { cn } from '@/lib/utils' const page = () => { + const { isExpanded } = useSidebar(); return (
{/* Desktop Sidebar */} {/* Main Content */} -
+
{/* Header */}
diff --git a/app/analytics/page.tsx b/app/analytics/page.tsx index e27f866..bba5214 100644 --- a/app/analytics/page.tsx +++ b/app/analytics/page.tsx @@ -9,10 +9,13 @@ import TransactionTable from '../../components/analytics/TransactionTable' import CategoryBreakdown from '../../components/analytics/CategoryBreakdown' import KeyInsights from '../../components/analytics/KeyInsights' import { DateRange } from '@/types/analytics.types' +import { useSidebar } from '@/context/SidebarContext' +import { cn } from '@/lib/utils' type ViewType = 'daily' | 'weekly' | 'monthly' | 'yearly' const AnalyticsPage = () => { + const { isExpanded } = useSidebar(); // State for filters const [dateRange, setDateRange] = useState({ startDate: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000), // Last 30 days @@ -52,7 +55,10 @@ const AnalyticsPage = () => { {/* Main Content */} -
+
{/* Header */}
diff --git a/app/home/transfer/amount/page.tsx b/app/home/transfer/amount/page.tsx index e3ddde4..3fdf594 100644 --- a/app/home/transfer/amount/page.tsx +++ b/app/home/transfer/amount/page.tsx @@ -13,6 +13,8 @@ import { getUserIdFromToken, isTokenExpired } from "@/utils/jwtUtils"; import { PinSetupModal } from "@/components/PinSetupModal"; import { PinResetModal } from "@/components/PinResetModal"; import { getCurrentUserInfo } from "@/utils/tokenUtils"; +import { useSidebar } from "@/context/SidebarContext"; +import { cn } from "@/lib/utils"; interface Recipient { id: string; @@ -25,6 +27,7 @@ interface Recipient { const AmountPage = () => { const router = useRouter(); + const { isExpanded } = useSidebar(); const [amount, setAmount] = useState(""); const [pin, setPin] = useState(""); const [showPin, setShowPin] = useState(false); @@ -383,7 +386,10 @@ const AmountPage = () => { {/* Header */} -
+
{/* Main Content */} -
+
{checkingPinStatus ? ( /* Loading State */
diff --git a/app/home/transfer/confirmation/page.tsx b/app/home/transfer/confirmation/page.tsx index 4107f90..7ffa24e 100644 --- a/app/home/transfer/confirmation/page.tsx +++ b/app/home/transfer/confirmation/page.tsx @@ -9,6 +9,8 @@ import { transferMoney, getUserBalance } from "@/helpers/api"; import { useAuthToken } from "@/hooks/use-auth-token"; import { getUserIdFromToken, isTokenExpired } from "@/utils/jwtUtils"; import { getCurrentUserInfo } from "@/utils/tokenUtils"; +import { useSidebar } from "@/context/SidebarContext"; +import { cn } from "@/lib/utils"; interface Recipient { id: string; @@ -21,6 +23,7 @@ interface Recipient { const ConfirmationPage = () => { const router = useRouter(); + const { isExpanded } = useSidebar(); const [amount, setAmount] = useState("5000"); const [recipient, setRecipient] = useState(null); const [isLoading, setIsLoading] = useState(false); @@ -139,7 +142,10 @@ const ConfirmationPage = () => { {/* Header */} -
+
@@ -147,7 +153,10 @@ const ConfirmationPage = () => {
{/* Main Content */} -
+
{/* Success Indicator */}
diff --git a/app/settings/page.tsx b/app/settings/page.tsx index 21818df..5d13f55 100644 --- a/app/settings/page.tsx +++ b/app/settings/page.tsx @@ -10,13 +10,19 @@ import { SecurityTab } from "@/components/settings/SecurityTab" import { NotificationsTab } from "@/components/settings/NotificationsTab" import { PaymentTab } from "@/components/settings/PaymentTab" import { PrivacyTab } from "@/components/settings/PrivacyTab" +import { useSidebar } from "@/context/SidebarContext" +import { cn } from "@/lib/utils" export default function SettingsPage() { + const { isExpanded } = useSidebar(); return (
-
+

Settings

Manage your profile settings and preferences

diff --git a/app/statistics/page.tsx b/app/statistics/page.tsx index 5f10b24..1c5e124 100644 --- a/app/statistics/page.tsx +++ b/app/statistics/page.tsx @@ -1,3 +1,4 @@ +'use client' import AccountInfo from '@/components/AccountInfo' import Dashboard from '@/components/Dashboard/Dashboard' @@ -8,15 +9,21 @@ import RecentMessages from '@/components/RecentMessages' import RecentTransactions from '@/components/RecentTransactions' import React from 'react' +import { useSidebar } from '@/context/SidebarContext' +import { cn } from '@/lib/utils' const page = () => { + const { isExpanded } = useSidebar(); return (
{/* Desktop Sidebar */} {/* Main Content */} -
+
{/* Header */}
diff --git a/app/transactions/page.tsx b/app/transactions/page.tsx index 8cdfb8c..e93fc43 100644 --- a/app/transactions/page.tsx +++ b/app/transactions/page.tsx @@ -2,14 +2,20 @@ import { TransactionList } from '@/components/Dashboard/TransactionList'; import Navigation from '@/components/Navigation'; +import { useSidebar } from '@/context/SidebarContext'; +import { cn } from '@/lib/utils'; export default function TransactionsPage() { + const { isExpanded } = useSidebar(); return (
{/* Sidebar navigation */} {/* Main content */} -
+

All Transactions

diff --git a/components/ClientProvider.tsx b/components/ClientProvider.tsx index 9df1367..f371b0f 100644 --- a/components/ClientProvider.tsx +++ b/components/ClientProvider.tsx @@ -6,6 +6,7 @@ import store from "@/lib/redux-store" import { Provider } from "react-redux" import { NotificationProvider } from "@/context/NotificationContext" import { ChatProvider } from "@/context/ChatContext" +import { SidebarProvider } from "@/context/SidebarContext" const ClientProvider = ({ children }: { children: React.ReactNode }) => { @@ -13,7 +14,9 @@ const ClientProvider = ({ children }: { children: React.ReactNode }) => { - {children} + + {children} + diff --git a/components/HomePageLayout.tsx b/components/HomePageLayout.tsx index aa4fef6..c6cd69c 100644 --- a/components/HomePageLayout.tsx +++ b/components/HomePageLayout.tsx @@ -8,11 +8,14 @@ import { useParams, useRouter } from 'next/navigation'; import RecentActions from "./RecentActions"; import Navigation from "./Navigation"; import { useAuthToken } from '@/hooks/use-auth-token'; +import { useSidebar } from '@/context/SidebarContext'; +import { cn } from '@/lib/utils'; export const HomePageLayout = () => { const params = useParams(); const router = useRouter(); const { getToken } = useAuthToken(); + const { isExpanded } = useSidebar(); const [userId, setUserId] = useState(""); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); @@ -192,7 +195,10 @@ export const HomePageLayout = () => { {/* Main Content */} -
+
{/* Header */}
diff --git a/components/Navigation.tsx b/components/Navigation.tsx index 6b3f904..a41a4f6 100644 --- a/components/Navigation.tsx +++ b/components/Navigation.tsx @@ -16,9 +16,14 @@ import { MessageCircle, ScanLine, CreditCard, + PanelLeftClose, + PanelLeft, + TrendingUp, + Wallet } from "lucide-react" import { cn } from "@/lib/utils" import { useAuthToken } from "@/hooks/use-auth-token" +import { useSidebar } from "@/context/SidebarContext" interface NavigationItem { id: string @@ -29,7 +34,7 @@ interface NavigationItem { } export default function Navigation() { - const [isExpanded, setIsExpanded] = useState(false) + const { isExpanded, toggleSidebar } = useSidebar() const [activeItem, setActiveItem] = useState("Home") const [userId, setUserId] = useState("") const [isReady, setIsReady] = useState(false) @@ -71,17 +76,17 @@ export default function Navigation() { const navigationItems: NavigationItem[] = [ { id: "Home", icon: , label: "Home", path: userId ? `/home/${userId}` : '/home' }, - { id: "Statistics", icon: , label: "Statistics", path: userId ? `/statistics/${userId}` : '/statistics' }, + { id: "Statistics", icon: , label: "Statistics", path: userId ? `/statistics/${userId}` : '/statistics' }, { id: "Scan", icon: , label: "Scan", path: "", isCenterButton: true }, { id: "Actions", icon: , label: "Actions", path: userId ? `/action/${userId}` : '/action' }, { id: "Chat", icon: , label: "Chat", path: userId ? `/chat` : '/chat' }, - { id: "Transactions", icon: , label: "Transactions", path: "/transactions" }, + { id: "Transactions", icon: , label: "Transactions", path: "/transactions" }, ] const mainMenuItems: NavigationItem[] = [ { id: "Home", icon: , label: "Home", path: userId ? `/home/${userId}` : '/home' }, - { id: "Statistics", icon: , label: "Statistics", path: userId ? `/statistics/${userId}` : '/statistics' }, - { id: "Transactions", icon: , label: "Transactions", path: "/transactions" }, + { id: "Statistics", icon: , label: "Statistics", path: userId ? `/statistics/${userId}` : '/statistics' }, + { id: "Transactions", icon: , label: "Transactions", path: "/transactions" }, { id: "Actions", icon: , label: "Action", path: userId ? `/action/${userId}` : '/action' }, { id: "Chat", icon: , label: "Chat", path: userId ? `/chat` : '/chat' }, ] @@ -125,21 +130,30 @@ export default function Navigation() { isExpanded ? "w-64" : "w-20", )} > - -
-
+ {/* Logo and Toggle Section */} +
{isExpanded ? ( -
LOGO
+ <> +
+ QueCode +
+ + ) : ( -
L
+ )}
@@ -150,9 +164,11 @@ export default function Navigation() { key={item.id} onClick={() => handleClick(item.id, item.path)} className={cn( - "w-[80%] flex items-center px-4 py-3 transition-colors mb-2", + "w-[80%] flex items-center px-3 py-2.5 transition-all mb-2 rounded-lg duration-200", isExpanded ? "justify-start" : "justify-center", - activeItem === item.id ? "bg-[#00B512] text-white rounded-r-full" : "text-white hover:bg-[#003D52]", + activeItem === item.id + ? "bg-gradient-to-r from-[#00B512] to-[#1fd331] text-white shadow-lg" + : "text-white hover:bg-[#004D5C] hover:shadow-md", )} > {item.icon} @@ -168,9 +184,11 @@ export default function Navigation() { key={item.id} onClick={() => handleClick(item.id, item.path)} className={cn( - "w-[80%] mx-auto flex items-center px-4 py-3 transition-colors mb-2", + "w-[80%] mx-auto flex items-center px-3 py-2.5 transition-all mb-2 rounded-lg duration-200", isExpanded ? "justify-start" : "justify-center", - activeItem === item.id ? "bg-[#00B512] text-white rounded-r-full" : "text-white hover:bg-[#003D52]", + activeItem === item.id + ? "bg-gradient-to-r from-[#00B512] to-[#1fd331] text-white shadow-lg" + : "text-white hover:bg-[#004D5C] hover:shadow-md", )} > {item.icon} diff --git a/components/chat/real-chat-page.tsx b/components/chat/real-chat-page.tsx index 4b8c5ce..aaa0bc2 100644 --- a/components/chat/real-chat-page.tsx +++ b/components/chat/real-chat-page.tsx @@ -8,6 +8,8 @@ import Navigation from '@/components/Navigation'; import { useChatOperations } from '@/hooks/use-chat-operations'; import { useChatModals } from '@/hooks/use-chat-modals'; import type { Chat, Conversation } from '@/types/chat.types'; +import { useSidebar } from '@/context/SidebarContext'; +import { cn } from '@/lib/utils'; import SendMoneyModal from '@/components/chat/send-money-modal'; import RequestMoneyModal from '@/components/chat/request-money-modal'; @@ -20,6 +22,7 @@ import InviteToGroupModal from '@/components/chat/invite-to-group-modal'; const { Content } = Layout; export default function RealChatPage() { + const { isExpanded } = useSidebar(); const { conversations, activeChat, @@ -86,7 +89,10 @@ export default function RealChatPage() {
-
+
{!isConnected && (
diff --git a/context/SidebarContext.tsx b/context/SidebarContext.tsx new file mode 100644 index 0000000..d328793 --- /dev/null +++ b/context/SidebarContext.tsx @@ -0,0 +1,50 @@ +"use client" + +import React, { createContext, useContext, useState, useEffect, type ReactNode } from 'react' + +interface SidebarContextType { + isExpanded: boolean + setIsExpanded: (expanded: boolean) => void + toggleSidebar: () => void +} + +const SidebarContext = createContext(undefined) + +export function SidebarProvider({ children }: { children: ReactNode }) { + const [isExpanded, setIsExpanded] = useState(false) + const [isMounted, setIsMounted] = useState(false) + + // Load sidebar state from localStorage on mount + useEffect(() => { + const savedState = localStorage.getItem('sidebarExpanded') + if (savedState !== null) { + setIsExpanded(savedState === 'true') + } + setIsMounted(true) + }, []) + + // Save sidebar state to localStorage whenever it changes + useEffect(() => { + if (isMounted) { + localStorage.setItem('sidebarExpanded', String(isExpanded)) + } + }, [isExpanded, isMounted]) + + const toggleSidebar = () => { + setIsExpanded(prev => !prev) + } + + return ( + + {children} + + ) +} + +export function useSidebar() { + const context = useContext(SidebarContext) + if (context === undefined) { + throw new Error('useSidebar must be used within a SidebarProvider') + } + return context +} diff --git a/package-lock.json b/package-lock.json index 584c0fc..2d9fe5e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "@radix-ui/react-hover-card": "^1.1.15", "@radix-ui/react-label": "^2.1.3", "@radix-ui/react-progress": "^1.1.2", + "@radix-ui/react-radio-group": "^1.3.8", "@radix-ui/react-scroll-area": "^1.2.9", "@radix-ui/react-select": "^2.1.6", "@radix-ui/react-separator": "^1.1.2", @@ -61,9 +62,15 @@ "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", + "@typescript-eslint/eslint-plugin": "^6.0.0", + "@typescript-eslint/parser": "^6.0.0", + "depcheck": "^1.4.7", "eslint": "^8", "eslint-config-next": "14.2.14", + "eslint-plugin-jsx-a11y": "^6.7.1", + "eslint-plugin-react-hooks": "^4.6.0", "postcss": "^8", + "prettier": "^3.0.0", "tailwindcss": "^3.4.1", "typescript": "^5" } @@ -194,6 +201,84 @@ "react": ">=16.9.0" } }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@babel/runtime": { "version": "7.28.4", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", @@ -203,6 +288,54 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@emnapi/core": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.5.0.tgz", @@ -1282,6 +1415,38 @@ } } }, + "node_modules/@radix-ui/react-radio-group": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz", + "integrity": "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-roving-focus": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", @@ -2110,6 +2275,13 @@ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", "license": "MIT" }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", @@ -2117,6 +2289,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/minimatch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "20.19.14", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.14.tgz", @@ -2133,6 +2312,13 @@ "integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==", "license": "MIT" }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/prop-types": { "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", @@ -2168,6 +2354,13 @@ "@types/react": "*" } }, + "node_modules/@types/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/use-sync-external-store": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", @@ -2181,160 +2374,124 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.43.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.43.0.tgz", - "integrity": "sha512-8tg+gt7ENL7KewsKMKDHXR1vm8tt9eMxjJBYINf6swonlWgkYn5NwyIgXpbbDxTNU5DgpDFfj95prcTq2clIQQ==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", + "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.43.0", - "@typescript-eslint/type-utils": "8.43.0", - "@typescript-eslint/utils": "8.43.0", - "@typescript-eslint/visitor-keys": "8.43.0", + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/type-utils": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", "graphemer": "^1.4.0", - "ignore": "^7.0.0", + "ignore": "^5.2.4", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.1.0" + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.43.0", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, "node_modules/@typescript-eslint/parser": { - "version": "8.43.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.43.0.tgz", - "integrity": "sha512-B7RIQiTsCBBmY+yW4+ILd6mF5h1FUwJsVvpqkrgpszYifetQ2Ke+Z4u6aZh0CblkUGIdR59iYVyXqqZGkZ3aBw==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", + "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", "dev": true, - "license": "MIT", + "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/scope-manager": "8.43.0", - "@typescript-eslint/types": "8.43.0", - "@typescript-eslint/typescript-estree": "8.43.0", - "@typescript-eslint/visitor-keys": "8.43.0", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", "debug": "^4.3.4" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/project-service": { - "version": "8.43.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.43.0.tgz", - "integrity": "sha512-htB/+D/BIGoNTQYffZw4uM4NzzuolCoaA/BusuSIcC8YjmBYQioew5VUZAYdAETPjeed0hqCaW7EHg+Robq8uw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.43.0", - "@typescript-eslint/types": "^8.43.0", - "debug": "^4.3.4" + "eslint": "^7.0.0 || ^8.0.0" }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.43.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.43.0.tgz", - "integrity": "sha512-daSWlQ87ZhsjrbMLvpuuMAt3y4ba57AuvadcR7f3nl8eS3BjRc8L9VLxFLk92RL5xdXOg6IQ+qKjjqNEimGuAg==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", + "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.43.0", - "@typescript-eslint/visitor-keys": "8.43.0" + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.43.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.43.0.tgz", - "integrity": "sha512-ALC2prjZcj2YqqL5X/bwWQmHA2em6/94GcbB/KKu5SX3EBDOsqztmmX1kMkvAJHzxk7TazKzJfFiEIagNV3qEA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.43.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.43.0.tgz", - "integrity": "sha512-qaH1uLBpBuBBuRf8c1mLJ6swOfzCXryhKND04Igr4pckzSEW9JX5Aw9AgW00kwfjWJF0kk0ps9ExKTfvXfw4Qg==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", + "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.43.0", - "@typescript-eslint/typescript-estree": "8.43.0", - "@typescript-eslint/utils": "8.43.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/utils": "6.21.0", "debug": "^4.3.4", - "ts-api-utils": "^2.1.0" + "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, "node_modules/@typescript-eslint/types": { - "version": "8.43.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.43.0.tgz", - "integrity": "sha512-vQ2FZaxJpydjSZJKiSW/LJsabFFvV7KgLC5DiLhkBcykhQj8iK9BOaDmQt74nnKdLvceM5xmhaTF+pLekrxEkw==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", + "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", "dev": true, "license": "MIT", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", @@ -2342,32 +2499,32 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.43.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.43.0.tgz", - "integrity": "sha512-7Vv6zlAhPb+cvEpP06WXXy/ZByph9iL6BQRBDj4kmBsW98AqEeQHlj/13X+sZOrKSo9/rNKH4Ul4f6EICREFdw==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", + "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", "dev": true, - "license": "MIT", + "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/project-service": "8.43.0", - "@typescript-eslint/tsconfig-utils": "8.43.0", - "@typescript-eslint/types": "8.43.0", - "@typescript-eslint/visitor-keys": "8.43.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", "debug": "^4.3.4", - "fast-glob": "^3.3.2", + "globby": "^11.1.0", "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.1.0" + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { @@ -2381,9 +2538,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", "dev": true, "license": "ISC", "dependencies": { @@ -2397,60 +2554,49 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.43.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.43.0.tgz", - "integrity": "sha512-S1/tEmkUeeswxd0GGcnwuVQPFWo8NzZTOMxCvw8BX7OMxnNae+i8Tm7REQen/SwUIPoPqfKn7EaZ+YLpiB3k9g==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", + "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.43.0", - "@typescript-eslint/types": "8.43.0", - "@typescript-eslint/typescript-estree": "8.43.0" + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "semver": "^7.5.4" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "eslint": "^7.0.0 || ^8.0.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.43.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.43.0.tgz", - "integrity": "sha512-T+S1KqRD4sg/bHfLwrpF/K3gQLBM1n7Rp7OjjikjTEssI2YJzQpi5WXoynOaQ93ERIuq3O8RBTOUYDKszUCEHw==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", + "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.43.0", - "eslint-visitor-keys": "^4.2.1" + "@typescript-eslint/types": "6.21.0", + "eslint-visitor-keys": "^3.4.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/@ungap/structured-clone": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", @@ -2727,6 +2873,67 @@ "win32" ] }, + "node_modules/@vue/compiler-core": { + "version": "3.5.25", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.25.tgz", + "integrity": "sha512-vay5/oQJdsNHmliWoZfHPoVZZRmnSWhug0BYT34njkYTPqClh3DNWLkZNJBVSjsNMrg0CCrBfoKkjZQPM/QVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@vue/shared": "3.5.25", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.25", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.25.tgz", + "integrity": "sha512-4We0OAcMZsKgYoGlMjzYvaoErltdFI2/25wqanuTu+S4gismOTRTBPi4IASOjxWdzIwrYSjnqONfKvuqkXzE2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.25", + "@vue/shared": "3.5.25" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.25", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.25.tgz", + "integrity": "sha512-PUgKp2rn8fFsI++lF2sO7gwO2d9Yj57Utr5yEsDf3GNaQcowCLKL7sf+LvVFvtJDXUp/03+dC6f2+LCv5aK1ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@vue/compiler-core": "3.5.25", + "@vue/compiler-dom": "3.5.25", + "@vue/compiler-ssr": "3.5.25", + "@vue/shared": "3.5.25", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.25", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.25.tgz", + "integrity": "sha512-ritPSKLBcParnsKYi+GNtbdbrIE1mtuFEJ4U1sWeuOMlIziK5GtOL85t5RhsNy4uWIXPgk+OUdpnXiTdzn8o3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.25", + "@vue/shared": "3.5.25" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.25", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.25.tgz", + "integrity": "sha512-AbOPdQQnAnzs58H2FrrDxYj/TJfmeS2jdfEEhgiKINy+bnOANmVizIEgq1r+C5zsbs6l1CCQxtcj71rwNQ4jWg==", + "dev": true, + "license": "MIT" + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -2953,6 +3160,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/array-differ": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-3.0.0.tgz", + "integrity": "sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/array-includes": { "version": "3.1.9", "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", @@ -2976,6 +3193,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/array.prototype.findlast": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", @@ -3096,6 +3323,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/ast-types-flow": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", @@ -3423,6 +3660,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/callsite": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", + "integrity": "sha512-0vdNRFXn5q+dtOqjfFtmtlI9N2eVZ7LMyEV2iKC5mEEFvSg/69Ml6b/WU2qF8W1nLRa0wiSrDT3Y5jOHZCwKPQ==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -3433,6 +3679,19 @@ "node": ">=6" } }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/camelcase-css": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", @@ -3589,6 +3848,74 @@ "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "license": "MIT" }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/clone": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", @@ -3679,6 +4006,33 @@ "toggle-selection": "^1.0.6" } }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cosmiconfig/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, "node_modules/crc-32": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", @@ -4020,6 +4374,124 @@ "node": ">=0.4.0" } }, + "node_modules/depcheck": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/depcheck/-/depcheck-1.4.7.tgz", + "integrity": "sha512-1lklS/bV5chOxwNKA/2XUUk/hPORp8zihZsXflr8x0kLwmcZ9Y9BsS6Hs3ssvA+2wUVbG0U2Ciqvm1SokNjPkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.23.0", + "@babel/traverse": "^7.23.2", + "@vue/compiler-sfc": "^3.3.4", + "callsite": "^1.0.0", + "camelcase": "^6.3.0", + "cosmiconfig": "^7.1.0", + "debug": "^4.3.4", + "deps-regex": "^0.2.0", + "findup-sync": "^5.0.0", + "ignore": "^5.2.4", + "is-core-module": "^2.12.0", + "js-yaml": "^3.14.1", + "json5": "^2.2.3", + "lodash": "^4.17.21", + "minimatch": "^7.4.6", + "multimatch": "^5.0.0", + "please-upgrade-node": "^3.2.0", + "readdirp": "^3.6.0", + "require-package-name": "^2.0.1", + "resolve": "^1.22.3", + "resolve-from": "^5.0.0", + "semver": "^7.5.4", + "yargs": "^16.2.0" + }, + "bin": { + "depcheck": "bin/depcheck.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/depcheck/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/depcheck/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/depcheck/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/depcheck/node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/depcheck/node_modules/minimatch": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-7.4.6.tgz", + "integrity": "sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/depcheck/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/deps-regex": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/deps-regex/-/deps-regex-0.2.0.tgz", + "integrity": "sha512-PwuBojGMQAYbWkMXOY9Pd/NWCDNHVH12pnS7WHqZkTSeMESe4hwnKKRp0yR87g37113x4JPbo/oIvXY+s/f56Q==", + "dev": true, + "license": "MIT" + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -4029,6 +4501,16 @@ "node": ">=6" } }, + "node_modules/detect-file": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", + "integrity": "sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/detect-node-es": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", @@ -4041,6 +4523,19 @@ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", "license": "Apache-2.0" }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", @@ -4137,8 +4632,31 @@ "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", "license": "MIT", - "engines": { - "node": ">=10.0.0" + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" } }, "node_modules/es-abstract": { @@ -4637,9 +5155,9 @@ } }, "node_modules/eslint-plugin-react-hooks": { - "version": "5.0.0-canary-7118f5dd7-20230705", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.0.0-canary-7118f5dd7-20230705.tgz", - "integrity": "sha512-AZYbMo/NW9chdL7vk6HQzQhT+PvTAEVqWk9ziruUoW2kAOcN5qNyelv70e0F1VNQAbvutOC9oc+xfWycI9FxDw==", + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", + "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", "dev": true, "license": "MIT", "engines": { @@ -4771,6 +5289,20 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/esquery": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", @@ -4807,6 +5339,13 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -4847,6 +5386,19 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, + "node_modules/expand-tilde": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", + "integrity": "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "homedir-polyfill": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -4980,6 +5532,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/findup-sync": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-5.0.0.tgz", + "integrity": "sha512-MzwXju70AuyflbgeOhzvQWAvvQdo1XL0A9bVvlXsYcFEBM87WR4OakL4OfZq+QRmr+duJubio+UtNQCPsVESzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-file": "^1.0.0", + "is-glob": "^4.0.3", + "micromatch": "^4.0.4", + "resolve-dir": "^1.0.1" + }, + "engines": { + "node": ">= 10.13.0" + } + }, "node_modules/flat-cache": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", @@ -5193,6 +5761,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -5341,6 +5919,51 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/global-modules": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", + "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", + "dev": true, + "license": "MIT", + "dependencies": { + "global-prefix": "^1.0.1", + "is-windows": "^1.0.1", + "resolve-dir": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/global-prefix": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", + "integrity": "sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "expand-tilde": "^2.0.2", + "homedir-polyfill": "^1.0.1", + "ini": "^1.3.4", + "is-windows": "^1.0.1", + "which": "^1.2.14" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/global-prefix/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, "node_modules/globals": { "version": "13.24.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", @@ -5374,6 +5997,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -5490,6 +6134,19 @@ "node": ">= 0.4" } }, + "node_modules/homedir-polyfill": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", + "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse-passwd": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/human-signals": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", @@ -5587,6 +6244,13 @@ "dev": true, "license": "ISC" }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -5638,6 +6302,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, "node_modules/is-async-function": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", @@ -6093,6 +6764,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", @@ -6179,6 +6860,19 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -6186,6 +6880,13 @@ "dev": true, "license": "MIT" }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -6420,6 +7121,16 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -6539,6 +7250,26 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/multimatch": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-5.0.0.tgz", + "integrity": "sha512-ypMKuglUrZUD99Tk2bUQ+xNQj43lPEfAeX2o9cTteAmShXy2VHDJpuwu1o0xqoKCt9jLVAvwyFKdLTPXKAfJyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/minimatch": "^3.0.3", + "array-differ": "^3.0.0", + "array-union": "^2.1.0", + "arrify": "^2.0.1", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", @@ -7053,6 +7784,35 @@ "node": ">=6" } }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-passwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", + "integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -7104,6 +7864,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -7140,6 +7910,16 @@ "node": ">= 6" } }, + "node_modules/please-upgrade-node": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz", + "integrity": "sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver-compare": "^1.0.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -7268,6 +8048,22 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -8363,6 +9159,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-package-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/require-package-name/-/require-package-name-2.0.1.tgz", + "integrity": "sha512-uuoJ1hU/k6M0779t3VMVIYpb2VMJk05cehCaABFhXaibcbvfgR8wKiozLjVFSzJPmQMRqIcO0HMyTFqfV09V6Q==", + "dev": true, + "license": "MIT" + }, "node_modules/reselect": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", @@ -8395,6 +9208,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-dir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", + "integrity": "sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "expand-tilde": "^2.0.0", + "global-modules": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -8637,6 +9464,13 @@ "node": ">=10" } }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "dev": true, + "license": "MIT" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -8797,6 +9631,16 @@ "dev": true, "license": "MIT" }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/socket.io-client": { "version": "4.8.1", "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz", @@ -8868,6 +9712,13 @@ "node": ">=0.10.0" } }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/ssf": { "version": "0.11.2", "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", @@ -9466,16 +10317,16 @@ "license": "MIT" }, "node_modules/ts-api-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", - "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", "dev": true, "license": "MIT", "engines": { - "node": ">=18.12" + "node": ">=16" }, "peerDependencies": { - "typescript": ">=4.8.4" + "typescript": ">=4.2.0" } }, "node_modules/ts-interface-checker": { @@ -10142,6 +10993,16 @@ "node": ">=0.4.0" } }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yaml": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", @@ -10154,6 +11015,57 @@ "node": ">= 14.6" } }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", From 4f0d2307c5e2cad14548cbc703ef89dd358ebe74 Mon Sep 17 00:00:00 2001 From: Olivier BYIRINGIRO Date: Mon, 1 Dec 2025 16:48:46 +0200 Subject: [PATCH 2/6] feat: Enhance Account Info and Header components with new features and UI improvements --- components/AccountInfo.tsx | 142 ++++++++++--------- components/Header.tsx | 32 ++--- components/HomePageLayout.tsx | 6 +- components/Navigation.tsx | 85 +++++++---- components/RecentActions.tsx | 225 +++++++----------------------- components/RecentMessages.tsx | 94 ++++--------- components/RecentTransactions.tsx | 89 ++++-------- components/WelcomeSection.tsx | 125 +++++++++++++++++ 8 files changed, 378 insertions(+), 420 deletions(-) create mode 100644 components/WelcomeSection.tsx diff --git a/components/AccountInfo.tsx b/components/AccountInfo.tsx index 288f2e2..1fe8c7a 100644 --- a/components/AccountInfo.tsx +++ b/components/AccountInfo.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react'; import axios from 'axios'; import { useRouter } from 'next/navigation'; -import { Copy, CreditCard, Send, Share2 } from 'lucide-react'; +import { Copy, CreditCard, Send, Share2, User, Download, Square, Plus } from 'lucide-react'; import baseUrl from '@/helpers/baseUrl'; import { getUserBalance, getEntityBalance } from '@/helpers/api'; @@ -213,89 +213,87 @@ const AccountInfo: React.FC = ({ userId }) => { return ( -
-
-

Hello👋 Welcome Back!!

-

- {user.firstName} {user.lastName} -

- - {/* Balance Display */} -
-

Available Balance

-

- {balanceLoading ? 'Loading...' : balanceError ? balanceError : `RWF ${balance?.toLocaleString()}`} -

-
- +12.5% this month -
-
+
+ {/* Header with Profile Link */} +
+
-
-
-
- {user.qrCode ? ( - QR Code - ) : ( -
- No QR Code -
- )} -
+ {/* QR Code Section */} +
+
+ {user.qrCode ? ( + QR Code + ) : ( +
+ No QR Code +
+ )}
-
-
-

- +

+ + - -
- - + + + +
-
+ {/* Action Buttons */} +
+ + + + -
diff --git a/components/Header.tsx b/components/Header.tsx index bd3a7fd..5812e60 100644 --- a/components/Header.tsx +++ b/components/Header.tsx @@ -1,6 +1,6 @@ "use client"; import React, { useEffect, useState } from "react"; -import { Eye, EyeOff, User, Wifi, WifiOff } from "lucide-react"; +import { Eye, EyeOff, User, Wifi, WifiOff, MessageCircle } from "lucide-react"; import { useRouter } from "next/navigation"; import axios from "axios"; import Image from "next/image"; @@ -11,6 +11,7 @@ import { useNotifications } from "@/context/NotificationContext"; import { useChat } from "@/context/ChatContext"; import NotificationBell from "./notifications/NotificationBell"; import { useAuthToken } from "@/hooks/use-auth-token"; +import { Button } from "./ui/button"; export const Header = () => { @@ -114,29 +115,16 @@ export const Header = () => { }; return ( -
+
{/* Left Section: Amount */}

- {balanceLoading - ? 'Loading...' - : balanceError - ? balanceError - : isBalanceVisible - ? `RWF ${balance?.toLocaleString()}` - : '••••••••••'} + QiewCode

-
{/* Right Section: Connection Status, Notification & User Profile */} -
+
{/* Connection Status Indicator */}
{isConnected ? ( @@ -155,6 +143,16 @@ export const Header = () => { {/* Notification Dropdown */} + {/* Message Icon */} + + {/* User Profile with Dropdown */}
{/* Profile Picture */} diff --git a/components/HomePageLayout.tsx b/components/HomePageLayout.tsx index c6cd69c..9e0e4a1 100644 --- a/components/HomePageLayout.tsx +++ b/components/HomePageLayout.tsx @@ -2,6 +2,7 @@ import React, { useEffect, useState } from "react"; import AccountInfo from "./AccountInfo"; import { Header } from "./Header"; +import { WelcomeSection } from "./WelcomeSection"; import { RecentMessages } from "./RecentMessages"; import { RecentTransactions } from "./RecentTransactions"; import { useParams, useRouter } from 'next/navigation'; @@ -203,6 +204,9 @@ export const HomePageLayout = () => { {/* Header */}
+ {/* Welcome Section */} + + {/* Content */}
@@ -210,8 +214,8 @@ export const HomePageLayout = () => {
- +
diff --git a/components/Navigation.tsx b/components/Navigation.tsx index a41a4f6..d548bd1 100644 --- a/components/Navigation.tsx +++ b/components/Navigation.tsx @@ -3,7 +3,7 @@ import type React from "react" import { useState, useEffect } from "react" -import { useRouter, useParams } from "next/navigation" +import { useRouter, useParams, usePathname } from "next/navigation" import { Button } from "@/components/ui/button" import { Home, @@ -19,7 +19,10 @@ import { PanelLeftClose, PanelLeft, TrendingUp, - Wallet + Wallet, + Users, + Clock, + Store } from "lucide-react" import { cn } from "@/lib/utils" import { useAuthToken } from "@/hooks/use-auth-token" @@ -41,6 +44,7 @@ export default function Navigation() { const { getToken, removeToken } = useAuthToken() const router = useRouter() const params = useParams() + const pathname = usePathname() // Get userId from URL params or token useEffect(() => { @@ -69,6 +73,27 @@ export default function Navigation() { getUserId(); }, [params]); + // Update active item based on current pathname + useEffect(() => { + if (pathname.includes('/analytics')) { + setActiveItem('Finances'); + } else if (pathname.includes('/chat')) { + setActiveItem('Messages'); + } else if (pathname.includes('/contacts')) { + setActiveItem('Contacts'); + } else if (pathname.includes('/merchants')) { + setActiveItem('Merchants'); + } else if (pathname.includes('/transactions')) { + setActiveItem('History'); + } else if (pathname.includes('/wallet')) { + setActiveItem('Wallet'); + } else if (pathname.includes('/settings')) { + setActiveItem('Settings'); + } else if (pathname.includes('/home')) { + setActiveItem('Home'); + } + }, [pathname]); + // Don't render navigation items until we have userId if (!isReady) { return null; @@ -85,10 +110,12 @@ export default function Navigation() { const mainMenuItems: NavigationItem[] = [ { id: "Home", icon: , label: "Home", path: userId ? `/home/${userId}` : '/home' }, - { id: "Statistics", icon: , label: "Statistics", path: userId ? `/statistics/${userId}` : '/statistics' }, - { id: "Transactions", icon: , label: "Transactions", path: "/transactions" }, - { id: "Actions", icon: , label: "Action", path: userId ? `/action/${userId}` : '/action' }, - { id: "Chat", icon: , label: "Chat", path: userId ? `/chat` : '/chat' }, + { id: "Finances", icon: , label: "Finances", path: "/analytics" }, + { id: "Messages", icon: , label: "Messages", path: "/chat" }, + { id: "Contacts", icon: , label: "Contacts", path: userId ? `/contacts/${userId}` : '/contacts' }, + { id: "Merchants", icon: , label: "Merchants", path: userId ? `/merchants/${userId}` : '/merchants' }, + { id: "History", icon: , label: "History", path: "/transactions" }, + { id: "Wallet", icon: , label: "Wallet", path: userId ? `/wallet/${userId}` : '/wallet' }, ] const bottomMenuItems: NavigationItem[] = [ @@ -132,11 +159,11 @@ export default function Navigation() { >
{/* Logo and Toggle Section */} -
+
{isExpanded ? ( <>
- QueCode + QiewCode
-
- {bottomMenuItems.map((item) => ( - - ))} +
+
+ {bottomMenuItems.map((item) => ( + + ))} +
diff --git a/components/RecentActions.tsx b/components/RecentActions.tsx index a92fb56..20030a1 100644 --- a/components/RecentActions.tsx +++ b/components/RecentActions.tsx @@ -1,198 +1,81 @@ "use client" -import React, { useState } from 'react'; -import { CheckCircle2, ChevronDown, Calendar, DollarSign, Clock } from 'lucide-react'; +import React from 'react'; +import { Plus } from 'lucide-react'; export const RecentActions = () => { - const [expandedItems, setExpandedItems] = useState(new Set()); - - const toggleItem = (id: any) => { - setExpandedItems(prev => { - const newSet = new Set(prev); - if (newSet.has(id)) { - newSet.delete(id); - } else { - newSet.add(id); - } - return newSet; - }); - }; - - const actions = [ + const campaigns = [ { id: 1, - name: 'Claude House', - progress: 75, - amount: 'RWF 1,000,000', - dueDate: '12/11/2024', - completed: true, - details: { - startDate: '01/01/2024', - description: 'Housing project phase 1', - status: 'In Progress', - totalMilestones: 4, - completedMilestones: 3, - } + name: 'Family trip savings', + current: 1300, + goal: 2000, + daysLeft: 5, }, { id: 2, - name: 'Claude House', - progress: 75, - amount: 'RWF 1,000,000', - dueDate: '12/11/2024', - completed: true, - details: { - startDate: '01/01/2024', - description: 'Housing project phase 2', - status: 'In Progress', - totalMilestones: 4, - completedMilestones: 3, - } + name: "Sarah's birthday gift", + current: 450, + goal: 500, + daysLeft: 2, }, - { - id: 3, - name: 'Claude House', - progress: 75, - amount: 'RWF 1,000,000', - dueDate: '12/11/2024', - completed: true, - details: { - startDate: '01/01/2024', - description: 'Housing project phase 3', - status: 'In Progress', - totalMilestones: 4, - completedMilestones: 3, - } - } ]; - const missRwandaEntries = [ - { - id: 1, - name: 'AKALIZA Amanda', - date: '1.14.2020', - avatar: '/api/placeholder/32/32' - }, - { - id: 2, - name: 'Mutesi Jolie', - date: '1.14.2020', - avatar: '/api/placeholder/32/32' - } - ]; + const getProgressPercentage = (current: number, goal: number) => { + return (current / goal) * 100; + }; return ( -
-

Recent Actions

+
+ {/* Header */} +
+

+ Active campaigns & groups +

+ +
-
-
- {/* Claude House Actions */} - {actions.map((action) => ( + {/* Campaigns Grid */} +
+
+ {campaigns.map((campaign) => (
- - - {/* Expanded Details */} - {expandedItems.has(action.id) && ( -
-
-
- - Started: {action.details.startDate} -
- -
- - Amount: {action.amount} -
- -
- - Due: {action.dueDate} -
- -
-

Description:

-

{action.details.description}

-
- -
-

Progress:

-

{action.details.completedMilestones} of {action.details.totalMilestones} milestones completed

-
-
-
- )} +
))} +
- {/* Miss Rwanda Section */} -
- - - {expandedItems.has('miss-rwanda') && ( -
- {missRwandaEntries.map((entry) => ( -
-
- {entry.name} -
-
-

{entry.name}

-

{entry.date}

-
- -
- ))} -
- )} -
+ {/* View All Link */} +
+
diff --git a/components/RecentMessages.tsx b/components/RecentMessages.tsx index e67f175..451824c 100644 --- a/components/RecentMessages.tsx +++ b/components/RecentMessages.tsx @@ -17,7 +17,7 @@ export const RecentMessages: React.FC = () => { senderName: "John Doe", subject: "Meeting Reminder", preview: "Don't forget about the meeting scheduled for tomorrow at 10 AM.", - timestamp: "01/01/2025, 10:00 AM", + timestamp: "10:45", }, { id: 2, @@ -25,87 +25,47 @@ export const RecentMessages: React.FC = () => { senderName: "Jane Smith", subject: "Project Update", preview: "The project is progressing well; here are the latest updates...", - timestamp: "31/12/2024, 4:00 PM", + timestamp: "09:38", + }, + { + id: 3, + avatar: "/Images/Profile.png", + senderName: "Bob Johnson", + subject: "Follow-up", + preview: "Just checking in on the status of the proposal.", + timestamp: "Yesterday", }, ]; return ( -
-
-

Recent Messages

-
-
- - - - - - - - - {messages.map((message) => ( - - - - - ))} - -
MessagesTimestamp
-
-
- {message.senderName} -
-
-

{message.senderName}

-

{message.subject}

-

{message.preview}

-
-
-
- {message.timestamp} -
-
- -
+
{messages.map((message) => (
-
-
- {message.senderName} +
+
+ {message.senderName.charAt(0)}
-
-

{message.senderName}

- - {message.timestamp.split(',')[0]} +
+

{message.senderName}

+ + {message.timestamp}
-

{message.subject}

-

{message.preview}

+

{message.preview}

diff --git a/components/RecentTransactions.tsx b/components/RecentTransactions.tsx index 450a083..7efc78d 100644 --- a/components/RecentTransactions.tsx +++ b/components/RecentTransactions.tsx @@ -88,82 +88,43 @@ export const RecentTransactions: React.FC = () => { } return ( -
+
{/* Header */} -
-

Recent Transactions

+
+

Recent transactions

- {/* Desktop Table View */} -
- - - - - - - - - - {displayedTransactions.map((transaction) => { - const { amount, displayName, transactionType, isOutgoing } = getTransactionDisplayInfo(transaction); - return ( - - - - - - ); - })} - -
TransactionsAmountDate
-
-
- {isOutgoing ? '📤' : '📥'} -
-
-

{displayName}

-

{transactionType} • {transaction.status}

-
-
-
- - {amount < 0 ? '-' : '+'}RWF {isNaN(Math.abs(amount)) ? '0' : Math.abs(amount).toLocaleString()} - - - {new Date(transaction.createdAt).toLocaleDateString()} -
-
- {/* Mobile List View */} -
+ + {/* Mobile & Desktop List View */} +
{displayedTransactions.map((transaction) => { const { amount, displayName, transactionType, isOutgoing } = getTransactionDisplayInfo(transaction); return ( -
-
-
-
- {isOutgoing ? '📤' : '📥'} -
-
-

{displayName}

-

{transactionType} • {transaction.status}

+
+
+
+ {displayName.charAt(0)} +
+
+
+

{displayName}

+ + {new Date(transaction.createdAt).toLocaleDateString('en-US', { + month: 'numeric', + day: 'numeric' + })} +
+

+ {amount < 0 ? '-' : '+ '}€{isNaN(Math.abs(amount)) ? '0' : (Math.abs(amount) / 100).toFixed(2)} +

-
- {new Date(transaction.createdAt).toLocaleDateString()} - - {amount < 0 ? '-' : '+'}RWF {isNaN(Math.abs(amount)) ? '0' : Math.abs(amount).toLocaleString()} - -
); })} diff --git a/components/WelcomeSection.tsx b/components/WelcomeSection.tsx new file mode 100644 index 0000000..6d07187 --- /dev/null +++ b/components/WelcomeSection.tsx @@ -0,0 +1,125 @@ +"use client"; +import React, { useEffect, useState } from "react"; +import { Eye, EyeOff } from "lucide-react"; +import axios from "axios"; +import baseUrl from "@/helpers/baseUrl"; +import { getEntityBalance } from '@/helpers/api'; +import { useAuthToken } from "@/hooks/use-auth-token"; + +interface WelcomeSectionProps { + userId: string; +} + +export const WelcomeSection: React.FC = ({ userId }) => { + const [userName, setUserName] = useState("User"); + const [balance, setBalance] = useState(null); + const [isBalanceVisible, setIsBalanceVisible] = useState(true); + const [balanceLoading, setBalanceLoading] = useState(true); + const [balanceError, setBalanceError] = useState(null); + const { getToken } = useAuthToken(); + + // Fetch user name + useEffect(() => { + if (!userId) return; + const fetchUser = async () => { + try { + const authToken = getToken(); + const headers = authToken ? { Authorization: `Bearer ${authToken}` } : {}; + const res = await axios.get(`${baseUrl}/users/${userId}`, { headers }); + const data = res.data; + setUserName(data.firstName || data.name || "User"); + } catch (err) { + console.error("Error fetching user:", err); + } + }; + fetchUser(); + }, [userId, getToken]); + + // Fetch wallet balance + useEffect(() => { + if (!userId) return; + const fetchBalance = async () => { + setBalanceLoading(true); + setBalanceError(null); + try { + let response; + try { + response = await getEntityBalance(userId, 'user'); + } catch (userError) { + response = await getEntityBalance(userId, 'organization'); + } + + if (response.success && response.data) { + setBalance(Number(response.data.balance)); + } else { + setBalanceError('Unable to fetch balance'); + } + } catch (err) { + setBalanceError('Could not fetch balance'); + } finally { + setBalanceLoading(false); + } + }; + fetchBalance(); + }, [userId]); + + const getGreeting = () => { + const hour = new Date().getHours(); + if (hour < 12) return "Good morning"; + if (hour < 18) return "Good afternoon"; + return "Good evening"; + }; + + const getFormattedDate = () => { + const today = new Date(); + const options: Intl.DateTimeFormatOptions = { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric' }; + return `Today – ${today.toLocaleDateString('en-US', options)}`; + }; + + const toggleBalanceVisibility = () => { + setIsBalanceVisible(!isBalanceVisible); + }; + + return ( +
+
+
+

+ {getGreeting()}, {userName} +

+

+ Welcome back! Here's a quick overview of your QiewCode wallet. +

+ +
+

Total Balance

+
+

+ {balanceLoading + ? 'Loading...' + : balanceError + ? balanceError + : isBalanceVisible + ? `RWF ${balance?.toLocaleString()}` + : '••••••••••'} +

+ + + +
+
+
+ +
+

{getFormattedDate()}

+
+
+
+ ); +}; From 0e7ecb886ad2dd2bb764ca27542bcdeab6392eca Mon Sep 17 00:00:00 2001 From: Olivier BYIRINGIRO Date: Thu, 11 Dec 2025 17:59:30 +0200 Subject: [PATCH 3/6] feat: Implement dark mode support with ThemeContext and update Tailwind configuration --- app/globals.css | 216 ++++++++++++++++++++++++++++++++++ components/ClientProvider.tsx | 17 +-- context/ThemeContext.tsx | 49 ++++++++ tailwind.config.ts | 19 ++- 4 files changed, 293 insertions(+), 8 deletions(-) create mode 100644 context/ThemeContext.tsx diff --git a/app/globals.css b/app/globals.css index 1671bbf..e30181b 100644 --- a/app/globals.css +++ b/app/globals.css @@ -201,4 +201,220 @@ body { html { font-size: 16px; } +} + +/* Ant Design Dark Mode Styling */ +.dark { + /* Select Component */ + .ant-select-selector { + @apply dark:bg-darkBg-main dark:border-darkBorder-light dark:text-white !important; + } + + .ant-select-selector:hover { + @apply dark:border-darkBorder-medium !important; + } + + .ant-select-focused .ant-select-selector { + @apply dark:border-green-600 dark:shadow-none !important; + } + + .ant-select-arrow { + @apply dark:text-gray-400 !important; + } + + .ant-select-clear { + @apply dark:text-gray-400 dark:hover:text-gray-300 !important; + } + + /* Select Dropdown Menu */ + .ant-select-dropdown { + @apply dark:bg-darkBg-card dark:border-darkBorder-light !important; + } + + .ant-select-item { + @apply dark:text-white dark:hover:bg-darkBg-main !important; + } + + .ant-select-item-option-selected { + @apply dark:bg-darkBg-main dark:text-white !important; + } + + /* DatePicker Component */ + .ant-picker { + @apply dark:bg-darkBg-main dark:border-darkBorder-light !important; + } + + .ant-picker:hover { + @apply dark:border-darkBorder-medium !important; + } + + .ant-picker-focused { + @apply dark:border-green-600 dark:shadow-none !important; + } + + .ant-picker-input { + @apply dark:text-white !important; + } + + .ant-picker-input::placeholder { + @apply dark:text-gray-500 !important; + } + + .ant-picker-suffix { + @apply dark:text-gray-400 !important; + } + + /* DatePicker Dropdown */ + .ant-picker-dropdown { + @apply dark:bg-darkBg-card dark:border-darkBorder-light !important; + } + + .ant-picker-header { + @apply dark:text-white !important; + } + + .ant-picker-header-button { + @apply dark:text-gray-300 dark:hover:text-white !important; + } + + .ant-picker-header-view { + @apply dark:text-white !important; + } + + .ant-picker-cell { + @apply dark:text-gray-200 !important; + } + + .ant-picker-cell-in-view { + @apply dark:text-white !important; + } + + .ant-picker-cell:hover:not(.ant-picker-cell-disabled) { + @apply dark:bg-darkBg-main !important; + } + + .ant-picker-cell-selected { + @apply dark:bg-green-700 dark:text-white !important; + } + + .ant-picker-cell-in-view.ant-picker-cell-selected { + @apply dark:bg-green-700 dark:text-white !important; + } + + .ant-picker-cell-in-view.ant-picker-cell-in-range { + @apply dark:bg-green-600 dark:text-white !important; + } + + .ant-picker-cell-range-start, + .ant-picker-cell-range-end { + @apply dark:bg-green-700 dark:text-white !important; + } + + .ant-picker-cell-range-start.ant-picker-cell-in-range, + .ant-picker-cell-range-end.ant-picker-cell-in-range { + @apply dark:bg-green-700 dark:text-white !important; + } + + /* Range hover effect */ + .ant-picker-cell-in-range:hover { + @apply dark:bg-green-500 dark:text-white !important; + } + + /* Range background - in between cells */ + .ant-picker-body .ant-picker-cell.ant-picker-cell-in-range::before { + @apply dark:bg-green-600/60 !important; + } + + .ant-picker-content { + @apply dark:bg-darkBg-card !important; + } + + /* DatePicker Range Separator */ + .ant-picker-range-separator { + @apply dark:text-gray-400 !important; + } + + /* DatePicker Panel */ + .ant-picker-panel { + @apply dark:bg-darkBg-card !important; + } + + /* DatePicker Month/Year */ + .ant-picker-month-panel, + .ant-picker-year-panel, + .ant-picker-decade-panel { + @apply dark:bg-darkBg-card !important; + } + + /* DatePicker Preset */ + .ant-picker-presets { + @apply dark:border-darkBorder-light !important; + } + + .ant-picker-presets-list { + @apply dark:bg-darkBg-card !important; + } + + .ant-picker-presets-item { + @apply dark:text-gray-300 dark:hover:text-white dark:hover:bg-darkBg-main !important; + } + + .ant-picker-presets-item-active { + @apply dark:text-green-400 dark:bg-darkBg-main !important; + } + + /* DatePicker Footer */ + .ant-picker-footer { + @apply dark:border-t dark:border-darkBorder-light !important; + } + + .ant-picker-footer-extra { + @apply dark:text-gray-300 !important; + } + + /* Ant Input */ + .ant-input { + @apply dark:bg-darkBg-main dark:border-darkBorder-light dark:text-white !important; + } + + .ant-input:hover { + @apply dark:border-darkBorder-medium !important; + } + + .ant-input-focused, + .ant-input:focus { + @apply dark:border-green-600 dark:shadow-none !important; + } + + .ant-input::placeholder { + @apply dark:text-gray-500 !important; + } + + /* Additional DatePicker Range styling */ + .ant-picker-range { + @apply dark:bg-darkBg-main !important; + } + + .ant-picker-range .ant-picker-input { + @apply dark:text-white !important; + } + + .ant-picker-range-separator { + @apply dark:text-gray-400 !important; + } + + /* DatePicker Disabled state */ + .ant-picker-cell-disabled, + .ant-picker-cell-disabled:hover { + @apply dark:text-gray-600 dark:cursor-not-allowed dark:bg-transparent !important; + } + + /* DatePicker Now button and OK button */ + .ant-btn-default { + @apply dark:border-darkBorder-light dark:text-gray-300 dark:hover:border-darkBorder-medium dark:hover:text-gray-200 !important; + } + + .ant-btn-primary { + @apply dark:bg-green-700 dark:border-green-700 dark:hover:bg-green-600 dark:hover:border-green-600 !important; + } } \ No newline at end of file diff --git a/components/ClientProvider.tsx b/components/ClientProvider.tsx index f371b0f..2732a6e 100644 --- a/components/ClientProvider.tsx +++ b/components/ClientProvider.tsx @@ -7,18 +7,21 @@ import { Provider } from "react-redux" import { NotificationProvider } from "@/context/NotificationContext" import { ChatProvider } from "@/context/ChatContext" import { SidebarProvider } from "@/context/SidebarContext" +import { ThemeProvider } from "@/context/ThemeContext" const ClientProvider = ({ children }: { children: React.ReactNode }) => { return ( - - - - {children} - - - + + + + + {children} + + + + ) } diff --git a/context/ThemeContext.tsx b/context/ThemeContext.tsx new file mode 100644 index 0000000..0d5de41 --- /dev/null +++ b/context/ThemeContext.tsx @@ -0,0 +1,49 @@ +"use client"; + +import React, { createContext, useContext, useEffect, useState } from "react"; + +type Theme = "light" | "dark"; + +interface ThemeContextType { + theme: Theme; + toggleTheme: () => void; +} + +const ThemeContext = createContext(undefined); + +export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const [theme, setTheme] = useState("light"); + + useEffect(() => { + // Check local storage or system preference + const savedTheme = localStorage.getItem("theme") as Theme; + if (savedTheme) { + setTheme(savedTheme); + document.documentElement.classList.toggle("dark", savedTheme === "dark"); + } else if (window.matchMedia("(prefers-color-scheme: dark)").matches) { + setTheme("dark"); + document.documentElement.classList.add("dark"); + } + }, []); + + const toggleTheme = () => { + const newTheme = theme === "light" ? "dark" : "light"; + setTheme(newTheme); + localStorage.setItem("theme", newTheme); + document.documentElement.classList.toggle("dark", newTheme === "dark"); + }; + + return ( + + {children} + + ); +}; + +export const useTheme = () => { + const context = useContext(ThemeContext); + if (context === undefined) { + throw new Error("useTheme must be used within a ThemeProvider"); + } + return context; +}; diff --git a/tailwind.config.ts b/tailwind.config.ts index 994bb40..391eb0d 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -48,7 +48,24 @@ const config: Config = { }, sidebar: { DEFAULT: '#00313A', - dark: '#003D52', + dark: '#0a1d15', + }, + // Brand Colors + brand: { + green: '#00B512', + gold: '#D4AF37', + goldHover: '#C9A530', + }, + // Dark Mode Theme + darkBg: { + main: '#0c221a', + card: '#143d2e', + interactive: '#143d2e', + }, + darkBorder: { + light: '#1c4d3b', + medium: '#25614b', + hover: '#2e755c', }, }, borderRadius: { From 2226209c0db1bbfbcf06536468d480a5556d2ac8 Mon Sep 17 00:00:00 2001 From: Olivier BYIRINGIRO Date: Thu, 11 Dec 2025 17:59:48 +0200 Subject: [PATCH 4/6] Enhance dark mode support across analytics components - Updated ComparisonChart to include dark mode styles for loading and error states, as well as text colors for various elements. - Modified FiltersBar to apply dark mode styles to card background, labels, and select inputs. - Improved KeyInsights with dark mode styles for card backgrounds and text colors. - Enhanced SummaryCards to support dark mode with appropriate background and text colors. - Updated TransactionTable to ensure all loading, error, and data display states are styled for dark mode. --- app/analytics/page.tsx | 6 +- components/analytics/CategoryBreakdown.tsx | 42 ++--- components/analytics/ComparisonChart.tsx | 52 +++--- components/analytics/FiltersBar.tsx | 12 +- components/analytics/KeyInsights.tsx | 44 ++--- components/analytics/SummaryCards.tsx | 30 ++-- components/analytics/TransactionTable.tsx | 180 ++++++++++----------- 7 files changed, 183 insertions(+), 183 deletions(-) diff --git a/app/analytics/page.tsx b/app/analytics/page.tsx index bba5214..69965b2 100644 --- a/app/analytics/page.tsx +++ b/app/analytics/page.tsx @@ -50,7 +50,7 @@ const AnalyticsPage = () => { } }; return ( -
+
{/* Desktop Sidebar */} @@ -67,8 +67,8 @@ const AnalyticsPage = () => {
{/* Page Title & Filters */}
-

Analytics

-

Track your spending patterns and financial insights.

+

Finances

+

Track your spending patterns and financial insights.

= ({ dateRange }) => { if (active && payload && payload.length) { const data = payload[0] return ( -
-

{data.payload.name}

-

+

+

{data.payload.name}

+

RWF {data.payload.amount.toLocaleString()} ({data.value}%)

@@ -53,19 +53,19 @@ const CategoryBreakdown: React.FC = ({ dateRange }) => { if (loading) { return ( - +
-
-
+
+
-
+
) } if (error) { return ( - +

Error loading category breakdown

) @@ -73,27 +73,27 @@ const CategoryBreakdown: React.FC = ({ dateRange }) => { if (categoryData.length === 0) { return ( - +

No category data available for the selected period

) } return ( - +
-

Category Breakdown

-

Spending distribution by categories

+

Category Breakdown

+

Spending distribution by categories

-

+

Click to browse or drag & drop an image file

{(fileUploadData.profileImageFile || fileUploadData.profileImagePreview) && (
{fileUploadData.profileImageFile && ( -
-
+
+
{fileUploadData.profileImageFile.name} selected for upload
-

+

Size: {formatFileSize(fileUploadData.profileImageFile.size)}

-

+

Type: {fileUploadData.profileImageFile.type.split('/')[1].toUpperCase()}

@@ -509,32 +513,32 @@ export const ProfileTab: React.FC = () => {
- + {/* Organization Logo Section */}
- +
handleFileInput(e, handleLogoChange)} /> {fileUploadData.logoFile && ( -
+
- - + + {fileUploadData.logoFile.name}
- + {/* Operational Document Section */}
- +
handleFileInput(e, handleOperationalDocumentChange)} /> {fileUploadData.operationalDocumentFile && ( -
+
- - + + {fileUploadData.operationalDocumentFile.name}
{/* File Requirements Info */} -
-

File Requirements

-
    +
    +

    File Requirements

    +
    • • Supported formats: PNG, JPG, JPEG, GIF, WebP, BMP, TIFF, JFIF, TIF
    • • Documents can also be PDF format
    • • Maximum file size: 5MB per file
    • @@ -593,12 +597,12 @@ export const ProfileTab: React.FC = () => { {/* Upload Status */} {(fileUploadData.profileImageFile || fileUploadData.logoFile || fileUploadData.operationalDocumentFile) && ( -
      -
      +
      +
      Files ready for upload
      -

      +

      Click "Save changes" to upload your selected files

      @@ -609,10 +613,10 @@ export const ProfileTab: React.FC = () => { {/* Account Verification Card */} - + - Account Verification - Verify your identity to unlock all features + Account Verification + Verify your identity to unlock all features
      diff --git a/components/settings/SecurityTab.tsx b/components/settings/SecurityTab.tsx index 8303517..b8fea4a 100644 --- a/components/settings/SecurityTab.tsx +++ b/components/settings/SecurityTab.tsx @@ -39,10 +39,10 @@ export const SecurityTab: React.FC = () => { return (
      - + - PIN Status - Your transaction PIN security status + PIN Status + Your transaction PIN security status {loadingPinStatus ? ( @@ -50,17 +50,17 @@ export const SecurityTab: React.FC = () => { ) : pinStatus ? (
      - PIN Setup: + PIN Setup:
      {pinStatus.hasPin ? ( <> - - Set up + + Set up ) : ( <> - - Not set up + + Not set up )}
      @@ -68,27 +68,27 @@ export const SecurityTab: React.FC = () => { {pinStatus.isLocked && (
      - Status: + Status:
      - - Locked + + Locked
      )}
      - Attempts Left: + Attempts Left: {pinStatus.attemptsLeft}/5
      {pinStatus.lockedUntil && ( -
      -

      +

      +

      PIN Locked: Too many failed attempts. Try again after {pinStatus.lockedUntil.toLocaleTimeString()}.

      @@ -96,32 +96,33 @@ export const SecurityTab: React.FC = () => { )}
      ) : ( -
      +
      Unable to load PIN status
      )} - + - Change Password - Update your password to keep your account secure + Change Password + Update your password to keep your account secure
      - +
      updateSecurityFormData({ currentPassword: e.target.value })} + className="dark:bg-darkBg-main dark:text-white dark:border-darkBorder-light" />
      - + updateSecurityFormData({ newPassword: e.target.value })} + className="dark:bg-darkBg-main dark:text-white dark:border-darkBorder-light" />
      - + updateSecurityFormData({ confirmPassword: e.target.value })} + className="dark:bg-darkBg-main dark:text-white dark:border-darkBorder-light" />
      - +
      - +
      { value={securityFormData.newPin} onChange={(e) => handleNumericInput(e.target.value, 'newPin')} placeholder="••••" + className="dark:bg-darkBg-main dark:text-white dark:border-darkBorder-light" />
      - + { value={securityFormData.confirmNewPin} onChange={(e) => handleNumericInput(e.target.value, 'confirmNewPin')} placeholder="••••" + className="dark:bg-darkBg-main dark:text-white dark:border-darkBorder-light" />
      - + + {/* Login Sessions Card - Full Width */} - + - Login Sessions - Manage your active sessions across devices + Login Sessions + Manage your active sessions across devices
      -
      +
      - +
      -

      Current device

      -

      +

      Current device

      +

      iPhone 13 • San Francisco, CA • Last active: Just now

      - + Current
      -
      +
      - +
      -

      Chrome on Windows

      -

      New York, NY • Last active: 2 days ago

      +

      Chrome on Windows

      +

      New York, NY • Last active: 2 days ago

      -
      -
      +
      - +
      -

      Android App

      -

      Chicago, IL • Last active: 5 days ago

      +

      Android App

      +

      Chicago, IL • Last active: 5 days ago

      -
      - - diff --git a/components/settings/shared/index.tsx b/components/settings/shared/index.tsx index d0869d7..9e0de02 100644 --- a/components/settings/shared/index.tsx +++ b/components/settings/shared/index.tsx @@ -17,7 +17,7 @@ export const LoadingSpinner: React.FC = ({ return (
      -
      +
      {text}
      @@ -33,7 +33,7 @@ interface ErrorMessageProps { export const ErrorMessage: React.FC = ({ message, onRetry }) => { return (
      -
      +
      @@ -41,7 +41,7 @@ export const ErrorMessage: React.FC = ({ message, onRetry }) {onRetry && ( @@ -58,15 +58,15 @@ interface SuccessMessageProps { export const SuccessMessage: React.FC = ({ message, details }) => { return ( -
      -
      +
      +
      {message}
      {details && ( -

      {details}

      +

      {details}

      )}
      ); @@ -87,9 +87,9 @@ export const VerificationCard: React.FC = ({ }) => { const statusConfig = { verified: { - bgColor: "bg-green-50", - textColor: "text-green-600", - badgeColor: "bg-green-100 text-green-800 border-green-200", + bgColor: "bg-green-50 dark:bg-green-900/20", + textColor: "text-green-600 dark:text-green-400", + badgeColor: "bg-green-100 dark:bg-green-900/40 text-green-800 dark:text-green-300 border-green-200 dark:border-green-800", icon: ( @@ -99,9 +99,9 @@ export const VerificationCard: React.FC = ({ showButton: false }, pending: { - bgColor: "bg-blue-50", - textColor: "text-blue-600", - badgeColor: "bg-blue-100 text-blue-800 border-blue-200", + bgColor: "bg-blue-50 dark:bg-blue-900/20", + textColor: "text-blue-600 dark:text-blue-400", + badgeColor: "bg-blue-100 dark:bg-blue-900/40 text-blue-800 dark:text-blue-300 border-blue-200 dark:border-blue-800", icon: ( @@ -111,9 +111,9 @@ export const VerificationCard: React.FC = ({ showButton: false }, required: { - bgColor: "bg-amber-50", - textColor: "text-amber-600", - badgeColor: "bg-amber-100 text-amber-800 border-amber-200", + bgColor: "bg-amber-50 dark:bg-amber-900/20", + textColor: "text-amber-600 dark:text-amber-400", + badgeColor: "bg-amber-100 dark:bg-amber-900/40 text-amber-800 dark:text-amber-300 border-amber-200 dark:border-amber-800", icon: ( @@ -133,8 +133,8 @@ export const VerificationCard: React.FC = ({ {config.icon}
      -

      {title}

      -

      {description}

      +

      {title}

      +

      {description}

      @@ -144,7 +144,7 @@ export const VerificationCard: React.FC = ({ {config.showButton && onAction && ( diff --git a/components/ui/Input-ant.tsx b/components/ui/Input-ant.tsx index 5de059c..6bc4e34 100644 --- a/components/ui/Input-ant.tsx +++ b/components/ui/Input-ant.tsx @@ -8,7 +8,13 @@ interface CustomInputProps extends InputProps { const Input: React.FC = ({ className, ...props }) => { return ( - + ) } From e00f25336fd8c5311c4d270c23620ad86d73c79a Mon Sep 17 00:00:00 2001 From: Olivier BYIRINGIRO Date: Thu, 11 Dec 2025 18:00:24 +0200 Subject: [PATCH 6/6] feat: Enhance RecentActions, RecentMessages, RecentTransactions, and WelcomeSection components with dark mode support and improved styling --- app/home/transfer/amount/page.tsx | 68 ++++--- components/AccountInfo.tsx | 54 ++--- components/Header.tsx | 60 ++++-- components/HomePageLayout.tsx | 36 ++-- components/Navigation.tsx | 159 ++++++++++----- components/PinResetModal.tsx | 51 ++++- components/RecentActions.tsx | 25 +-- components/RecentMessages.tsx | 191 +++++++++++++----- components/RecentTransactions.tsx | 178 +++++++++++++--- components/WelcomeSection.tsx | 16 +- components/notifications/NotificationBell.tsx | 2 +- .../notifications/NotificationCenter.tsx | 61 +++--- helpers/api.ts | 2 + types/dashboard.ts | 22 ++ 14 files changed, 657 insertions(+), 268 deletions(-) diff --git a/app/home/transfer/amount/page.tsx b/app/home/transfer/amount/page.tsx index 3fdf594..2861006 100644 --- a/app/home/transfer/amount/page.tsx +++ b/app/home/transfer/amount/page.tsx @@ -50,6 +50,7 @@ const AmountPage = () => { const { getToken } = useAuthToken(); const [showPinSetupModal, setShowPinSetupModal] = useState(false); const [showPinResetModal, setShowPinResetModal] = useState(false); + const [pinResetModalError, setPinResetModalError] = useState(""); const [checkingPinStatus, setCheckingPinStatus] = useState(true); const handlePinSetupSuccess = async () => { @@ -60,6 +61,7 @@ const AmountPage = () => { const handlePinResetSuccess = () => { // After PIN reset, close modal and allow user to try again setShowPinResetModal(false); + setPinResetModalError(""); setError(""); }; @@ -177,26 +179,29 @@ const AmountPage = () => { setCategoriesLoading(true); try { const response: any = await getTransactionCategories(); - if (response.success) { - setCategories(response.data); + + const categoryData = response.data?.data; + + if (categoryData && Array.isArray(categoryData) && categoryData.length > 0) { + setCategories(categoryData); // For organizations, automatically select organization category and disable constraints if (recipient?.type === 'organization' && organizationCategory) { - const orgCategory = response.data.find((cat: any) => cat.id === organizationCategory.id); + const orgCategory = categoryData.find((cat: any) => cat.id === organizationCategory.id); if (orgCategory) { setSelectedCategory(orgCategory); } setApplyConstraints(false); // Organizations don't use constraints } else if (recipient?.type !== 'organization') { // For individual users, use 'Other' as default - const defaultCategory = response.data.find((cat: any) => cat.name === 'Other'); + const defaultCategory = categoryData.find((cat: any) => cat.name === 'Other'); if (defaultCategory) { setSelectedCategory(defaultCategory); } } } } catch (err) { - console.error('Failed to load categories:', err); + console.error('Error fetching categories:', err); } finally { setCategoriesLoading(false); } @@ -351,8 +356,12 @@ const AmountPage = () => { return; } - // Check if PIN is locked - if (err?.response?.data?.message?.includes('locked') || err?.response?.data?.lockedUntil) { + const attemptsRemaining = err?.response?.data?.attemptsRemaining; + const isAccountLocked = attemptsRemaining === 0 || err?.response?.data?.message?.includes('Account locked') || err?.response?.data?.lockedUntil; + + if (isAccountLocked) { + const errorMessage = err?.response?.data?.message || 'Your account has been locked due to too many failed PIN attempts. Please reset your PIN to continue.'; + setPinResetModalError(errorMessage); setShowPinResetModal(true); setTransferInProgress(false); setLoading(false); @@ -361,11 +370,7 @@ const AmountPage = () => { // Handle PIN attempt errors with more specific feedback const errorMessage = err?.response?.data?.message || err?.message || 'Transfer failed'; - if (errorMessage.includes('PIN') && errorMessage.includes('attempt')) { - setError(`${errorMessage} You can reset your PIN if you've forgotten it.`); - } else { - setError(errorMessage); - } + setError(errorMessage); setTransferInProgress(false); setLoading(false); @@ -497,21 +502,31 @@ const AmountPage = () => { {categoriesLoading ? (
      Loading categories...
      - ) : ( -
      - {categories.map((category) => ( - - ))} + ) : categories.length === 0 ? ( +
      +

      No categories available

      +

      Please try refreshing the page

      + ) : ( + <> +
      + {categories.map((category) => ( + + ))} +
      +

      + Total categories: {categories.length} | Selected: {selectedCategory?.name || 'None'} +

      + )}
      )} @@ -749,6 +764,7 @@ const AmountPage = () => { open={showPinResetModal} onOpenChange={setShowPinResetModal} onSuccess={handlePinResetSuccess} + lockoutError={pinResetModalError} />
      ); diff --git a/components/AccountInfo.tsx b/components/AccountInfo.tsx index 1fe8c7a..5430910 100644 --- a/components/AccountInfo.tsx +++ b/components/AccountInfo.tsx @@ -172,10 +172,10 @@ const AccountInfo: React.FC = ({ userId }) => { if (loading) { return ( -
      +
      -
      -

      Loading user data...

      +
      +

      Loading user data...

      ); @@ -183,14 +183,14 @@ const AccountInfo: React.FC = ({ userId }) => { if (error) { return ( -
      +
      -

      {error}

      +

      {error}

      @@ -198,7 +198,7 @@ const AccountInfo: React.FC = ({ userId }) => { onClick={() => { router.push('/logout'); }} - className="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600" + className="px-4 py-2 bg-red-500 dark:bg-red-600 text-white rounded hover:opacity-90 transition font-medium" > Re-login @@ -213,12 +213,12 @@ const AccountInfo: React.FC = ({ userId }) => { return ( -
      +
      {/* Header with Profile Link */} -
      +
      - - -
      diff --git a/components/Header.tsx b/components/Header.tsx index 5812e60..616c255 100644 --- a/components/Header.tsx +++ b/components/Header.tsx @@ -1,6 +1,6 @@ "use client"; import React, { useEffect, useState } from "react"; -import { Eye, EyeOff, User, Wifi, WifiOff, MessageCircle } from "lucide-react"; +import { Eye, EyeOff, User, Wifi, WifiOff, MessageCircle, Moon, Sun } from "lucide-react"; import { useRouter } from "next/navigation"; import axios from "axios"; import Image from "next/image"; @@ -12,6 +12,7 @@ import { useChat } from "@/context/ChatContext"; import NotificationBell from "./notifications/NotificationBell"; import { useAuthToken } from "@/hooks/use-auth-token"; import { Button } from "./ui/button"; +import { useTheme } from "@/context/ThemeContext"; export const Header = () => { @@ -26,6 +27,7 @@ export const Header = () => { const router = useRouter(); const notifications = useNotifications(); const { getToken } = useAuthToken(); + const { theme, toggleTheme } = useTheme(); // Try to get chat context, but don't fail if it's not available let chat; @@ -115,33 +117,46 @@ export const Header = () => { }; return ( -
      - {/* Left Section: Amount */} -
      -

      +
      + {/* Left Section: Title */} +
      +

      QiewCode

      {/* Right Section: Connection Status, Notification & User Profile */} -
      +
      {/* Connection Status Indicator */} -
      +
      {isConnected ? ( -
      +
      Connected
      ) : ( -
      +
      Offline
      )}
      + {/* Theme Toggle */} + + {/* Notification Dropdown */} - +
      + +
      {/* Message Icon */} {/* User Profile with Dropdown */} -
      +
      {/* Profile Picture */} @@ -194,7 +210,7 @@ export const Header = () => {
    • @@ -202,7 +218,7 @@ export const Header = () => {
    • @@ -210,7 +226,7 @@ export const Header = () => {
    • diff --git a/components/HomePageLayout.tsx b/components/HomePageLayout.tsx index 9e0e4a1..de86a17 100644 --- a/components/HomePageLayout.tsx +++ b/components/HomePageLayout.tsx @@ -118,11 +118,11 @@ export const HomePageLayout = () => { // If too many redirect attempts, show error and manual redirect button if (redirectAttempts >= 3) { return ( -
      +
      -
      Redirect failed
      -

      Unable to automatically redirect to your home page.

      +
      Redirect failed
      +

      Unable to automatically redirect to your home page.

      @@ -157,18 +157,18 @@ export const HomePageLayout = () => { // Show loading state while determining userId or redirecting if (loading || isRedirecting) { return ( -
      +
      -
      -

      +

      +

      {isRedirecting ? 'Redirecting to your home page...' : 'Loading...'}

      {isRedirecting && ( -

      Please wait...

      +

      Please wait...

      )} {redirectAttempts > 0 && ( -

      Attempt {redirectAttempts + 1}/3

      +

      Attempt {redirectAttempts + 1}/3

      )}
      @@ -179,11 +179,11 @@ export const HomePageLayout = () => { // Show error state if (error) { return ( -
      +
      -
      {error}
      -

      Redirecting to login...

      +
      {error}
      +

      Redirecting to login...

      @@ -191,16 +191,16 @@ export const HomePageLayout = () => { } return ( -
      +
      {/* Desktop Sidebar */} {/* Main Content */}
      -
      +
      {/* Header */}
      @@ -208,12 +208,12 @@ export const HomePageLayout = () => { {/* Content */} -
      -
      +
      +
      -
      +
      diff --git a/components/Navigation.tsx b/components/Navigation.tsx index d548bd1..c1f304a 100644 --- a/components/Navigation.tsx +++ b/components/Navigation.tsx @@ -22,7 +22,9 @@ import { Wallet, Users, Clock, - Store + Store, + MoreHorizontal, + X } from "lucide-react" import { cn } from "@/lib/utils" import { useAuthToken } from "@/hooks/use-auth-token" @@ -41,6 +43,7 @@ export default function Navigation() { const [activeItem, setActiveItem] = useState("Home") const [userId, setUserId] = useState("") const [isReady, setIsReady] = useState(false) + const [isMoreMenuOpen, setIsMoreMenuOpen] = useState(false) const { getToken, removeToken } = useAuthToken() const router = useRouter() const params = useParams() @@ -99,13 +102,21 @@ export default function Navigation() { return null; } - const navigationItems: NavigationItem[] = [ + const mobilePrimaryItems: NavigationItem[] = [ { id: "Home", icon: , label: "Home", path: userId ? `/home/${userId}` : '/home' }, { id: "Statistics", icon: , label: "Statistics", path: userId ? `/statistics/${userId}` : '/statistics' }, { id: "Scan", icon: , label: "Scan", path: "", isCenterButton: true }, - { id: "Actions", icon: , label: "Actions", path: userId ? `/action/${userId}` : '/action' }, { id: "Chat", icon: , label: "Chat", path: userId ? `/chat` : '/chat' }, + { id: "More", icon: , label: "More", path: "" }, + ] + + const mobileSecondaryItems: NavigationItem[] = [ + { id: "Actions", icon: , label: "Actions", path: userId ? `/action/${userId}` : '/action' }, { id: "Transactions", icon: , label: "Transactions", path: "/transactions" }, + { id: "Contacts", icon: , label: "Contacts", path: userId ? `/contacts/${userId}` : '/contacts' }, + { id: "Merchants", icon: , label: "Merchants", path: userId ? `/merchants/${userId}` : '/merchants' }, + { id: "Wallet", icon: , label: "Wallet", path: userId ? `/wallet/${userId}` : '/wallet' }, + { id: "Settings", icon: , label: "Settings", path: `/settings` }, ] const mainMenuItems: NavigationItem[] = [ @@ -119,7 +130,7 @@ export default function Navigation() { ] const bottomMenuItems: NavigationItem[] = [ - { id: "Settings", icon: , label: "Settings", path: userId ? `/settings/${userId}` : '/settings' }, + { id: "Settings", icon: , label: "Settings", path: `/settings` }, { id: "Logout", icon: , label: "Logout", path: "/logout" }, ] @@ -138,8 +149,15 @@ export default function Navigation() { return; } + // Handle More button + if (id === "More") { + setIsMoreMenuOpen(!isMoreMenuOpen); + return; + } + // Always update the active item setActiveItem(id); + setIsMoreMenuOpen(false); // Navigate if there's a valid path if (path) { @@ -153,13 +171,13 @@ export default function Navigation() { {/* Desktop Sidebar */} {/* Mobile Bottom Navigation */} -
      + )} + +
      + {/* Left items */} +
      + + + +
      + + {/* Center Scan Button - Larger */} + + + {/* Right items */} +
      + + + +
      diff --git a/components/PinResetModal.tsx b/components/PinResetModal.tsx index 9e7bc10..aea4b3b 100644 --- a/components/PinResetModal.tsx +++ b/components/PinResetModal.tsx @@ -14,18 +14,20 @@ import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { AlertCircle, Loader2, Mail, MessageSquare } from "lucide-react"; import { useAuthToken } from "@/hooks/use-auth-token"; -import { requestPinReset, confirmPinReset } from "@/helpers/api"; +import { requestPinReset, confirmPinReset, validateResetToken } from "@/helpers/api"; interface PinResetModalProps { open: boolean; onOpenChange: (open: boolean) => void; onSuccess?: () => void; + lockoutError?: string; } export const PinResetModal: React.FC = ({ open, onOpenChange, onSuccess, + lockoutError, }) => { const { getToken } = useAuthToken(); const [step, setStep] = useState<'method' | 'verify' | 'new-pin'>('method'); @@ -54,6 +56,37 @@ export const PinResetModal: React.FC = ({ } }; + const handleVerifyResetToken = async (e: React.FormEvent) => { + e.preventDefault(); + setError(""); + + if (!resetToken) { + setError("Please enter your reset code"); + return; + } + + setLoading(true); + try { + // Call the dedicated validation endpoint + const response = await validateResetToken(resetToken); + + // Response is wrapped in response.data + if (response?.data?.success) { + // Token is valid, proceed to new PIN step + setStep('new-pin'); + } else { + // Token is invalid + const errorMsg = response?.data?.message || "Invalid or expired reset code"; + setError(errorMsg); + } + } catch (err: any) { + const errorMessage = err.response?.data?.message || "Failed to verify reset code. Please try again."; + setError(errorMessage); + } finally { + setLoading(false); + } + }; + const handleConfirmReset = async (e: React.FormEvent) => { e.preventDefault(); setError(""); @@ -117,6 +150,17 @@ export const PinResetModal: React.FC = ({ + {lockoutError && step === 'method' && ( +
      +
      + +
      +

      {lockoutError}

      +
      +
      +
      + )} + {step === 'method' && (
      @@ -163,7 +207,7 @@ export const PinResetModal: React.FC = ({ )} {step === 'verify' && ( -
      { e.preventDefault(); setStep('new-pin'); }} className="space-y-4"> +
      = ({ Back diff --git a/components/RecentActions.tsx b/components/RecentActions.tsx index 20030a1..b6b6ed2 100644 --- a/components/RecentActions.tsx +++ b/components/RecentActions.tsx @@ -25,44 +25,44 @@ export const RecentActions = () => { }; return ( -
      +
      {/* Header */} -
      -

      +
      +

      Active campaigns & groups

      -
      - + {/* Campaigns Grid */}
      {campaigns.map((campaign) => (
      -

      +

      {campaign.name}

      - + €{campaign.current.toLocaleString()} / €{campaign.goal.toLocaleString()} - + {campaign.daysLeft} days left
      -
      +
      @@ -73,8 +73,9 @@ export const RecentActions = () => { {/* View All Link */}
      -
      diff --git a/components/RecentMessages.tsx b/components/RecentMessages.tsx index 451824c..fc6579a 100644 --- a/components/RecentMessages.tsx +++ b/components/RecentMessages.tsx @@ -1,71 +1,168 @@ +"use client"; import Image from "next/image"; +import { useEffect, useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { useAuthToken } from '@/hooks/use-auth-token'; +import { useGetUserChatsQuery } from '@/states/chatSlice'; +import { MessageCircle, Send } from 'lucide-react'; -interface Message { - id: number; - avatar: string; - senderName: string; - subject: string; - preview: string; - timestamp: string; +interface ChatMessage { + id: string; + name: string; + lastMessage?: { + content: string; + createdAt: string; + sender: string; + }; + avatar?: string; } export const RecentMessages: React.FC = () => { - const messages: Message[] = [ - { - id: 1, - avatar: "/Images/Profile.png", - senderName: "John Doe", - subject: "Meeting Reminder", - preview: "Don't forget about the meeting scheduled for tomorrow at 10 AM.", - timestamp: "10:45", - }, - { - id: 2, - avatar: "/Images/Profile.png", - senderName: "Jane Smith", - subject: "Project Update", - preview: "The project is progressing well; here are the latest updates...", - timestamp: "09:38", - }, - { - id: 3, - avatar: "/Images/Profile.png", - senderName: "Bob Johnson", - subject: "Follow-up", - preview: "Just checking in on the status of the proposal.", - timestamp: "Yesterday", - }, - ]; + const router = useRouter(); + const { getToken } = useAuthToken(); + const token = getToken(); + const [conversations, setConversations] = useState([]); + + const { data: chatsData, isLoading, error } = useGetUserChatsQuery(undefined, { + skip: !token + }); + + useEffect(() => { + if (chatsData?.data) { + const formattedChats = chatsData.data.slice(0, 5).map((chat: any) => ({ + id: chat.id, + name: chat.name || 'Unknown Chat', + lastMessage: chat.lastMessage ? { + content: chat.lastMessage.content || '', + createdAt: chat.lastMessage.createdAt, + sender: chat.lastMessage.sender || 'Unknown' + } : undefined, + avatar: chat.avatar + })); + setConversations(formattedChats); + } + }, [chatsData]); + + const getTimeDisplay = (createdAt: string) => { + const messageDate = new Date(createdAt); + const today = new Date(); + const yesterday = new Date(today); + yesterday.setDate(yesterday.getDate() - 1); + + if (messageDate.toDateString() === today.toDateString()) { + return messageDate.toLocaleTimeString('en-US', { + hour: 'numeric', + minute: '2-digit', + hour12: true + }); + } else if (messageDate.toDateString() === yesterday.toDateString()) { + return 'Yesterday'; + } else { + return messageDate.toLocaleDateString('en-US', { + month: 'short', + day: 'numeric' + }); + } + }; + + const truncateText = (text: string, maxLength: number = 60) => { + return text.length > maxLength ? text.substring(0, maxLength) + '...' : text; + }; + + if (isLoading) { + return ( +
      +
      +

      + Recent messages +

      +
      +
      +
      +

      Loading messages...

      +
      +
      + ); + } + + if (error) { + return ( +
      +
      +

      + Recent messages +

      +
      +
      +

      Failed to load messages

      +

      Please try again later

      +
      +
      + ); + } + + if (!conversations || conversations.length === 0) { + return ( +
      +
      +

      + Recent messages +

      +
      +
      +
      + +
      +

      No conversations yet

      +

      Start a conversation to connect with your contacts

      + +
      +
      + ); + } return ( -
      -
      -

      +
      +
      +

      Recent messages

      -
      -
      - {messages.map((message) => ( +
      + {conversations.map((chat) => (
      router.push('/chat')} >
      -
      - {message.senderName.charAt(0)} +
      + {chat.name.charAt(0).toUpperCase()}
      -

      {message.senderName}

      - - {message.timestamp} +

      {chat.name}

      + + {chat.lastMessage ? getTimeDisplay(chat.lastMessage.createdAt) : ''}
      -

      {message.preview}

      +

      + {chat.lastMessage ? truncateText(chat.lastMessage.content) : 'No messages yet'} +

      diff --git a/components/RecentTransactions.tsx b/components/RecentTransactions.tsx index 7efc78d..111ce0f 100644 --- a/components/RecentTransactions.tsx +++ b/components/RecentTransactions.tsx @@ -5,6 +5,7 @@ import { Transaction } from '@/types/dashboard'; import { useRouter } from 'next/navigation'; import { useAuthToken } from '@/hooks/use-auth-token'; import { getUserIdFromToken, isTokenExpired } from '@/utils/jwtUtils'; +import { CheckCircle, Clock, AlertCircle } from 'lucide-react'; export const RecentTransactions: React.FC = () => { const [transactions, setTransactions] = useState([]); @@ -58,71 +59,198 @@ export const RecentTransactions: React.FC = () => { fetchTransactions(); }, []); + const getInitials = (name: string) => { + return name + .split(' ') + .map((part) => part.charAt(0).toUpperCase()) + .join('') + .slice(0, 2); + }; + const getTransactionDisplayInfo = (transaction: Transaction) => { - const isOutgoing = transaction.type === 'sent'; + // Determine if it's outgoing based on senderWallet.userId matching currentUserId + const isOutgoing = transaction.senderWallet?.userId === currentUserId; const transactionAmount = Number(transaction.amount) || 0; const transactionFee = Number(transaction.fee) || 0; const amount = isOutgoing ? -(transactionAmount + transactionFee) : transactionAmount; - const displayName = transaction.description || (isOutgoing ? 'Money Sent' : 'Money Received'); + + // Get recipient/sender name with better fallback logic + let counterpartyName = 'Transaction'; + let isToOrganization = false; + + if (isOutgoing) { + // Sending money - check receiver first + if (transaction.receiverWallet?.organization?.name) { + counterpartyName = transaction.receiverWallet.organization.name; + isToOrganization = true; + } else if (transaction.receiverWallet?.user?.firstName || transaction.receiverWallet?.user?.lastName) { + counterpartyName = `${transaction.receiverWallet.user.firstName || ''} ${transaction.receiverWallet.user.lastName || ''}`.trim(); + } else if (transaction.description) { + counterpartyName = transaction.description; + } else { + counterpartyName = 'Money Sent'; + } + } else { + // Receiving money - check sender first + if (transaction.senderWallet?.organization?.name) { + counterpartyName = transaction.senderWallet.organization.name; + } else if (transaction.senderWallet?.user?.firstName || transaction.senderWallet?.user?.lastName) { + counterpartyName = `${transaction.senderWallet.user.firstName || ''} ${transaction.senderWallet.user.lastName || ''}`.trim(); + } else if (transaction.description) { + counterpartyName = transaction.description; + } else { + counterpartyName = 'Money Received'; + } + } + + // Determine transaction status (assuming completed for now, can be enhanced based on API response) + const status = transaction.status || 'completed'; // 'completed', 'pending', 'failed' const transactionType = isOutgoing ? 'Sent' : 'Received'; - return { amount, displayName, transactionType, isOutgoing }; + return { amount, counterpartyName, transactionType, isOutgoing, isToOrganization, status }; }; if (loading) { - return
      Loading transactions...
      ; + return ( +
      +
      +

      Recent transactions

      +
      +
      +
      +

      Loading transactions...

      +
      +
      + ); } + if (error) { - return
      {error}
      ; + return ( +
      +
      +

      Recent transactions

      +
      +
      +

      {error}

      +

      Unable to load transactions

      +
      +
      + ); } + const displayedTransactions = transactions.slice(0, 5); if (displayedTransactions.length === 0) { return ( -
      -

      Recent Transactions

      -
      - No transactions found. Start by sending or receiving money! +
      +
      +

      Recent transactions

      +
      +
      +

      No transactions found.

      +

      Start by sending or receiving money!

      ); } return ( -
      +
      {/* Header */} -
      -

      Recent transactions

      +
      +

      Recent transactions

      {/* Mobile & Desktop List View */} -
      +
      {displayedTransactions.map((transaction) => { - const { amount, displayName, transactionType, isOutgoing } = getTransactionDisplayInfo(transaction); + const { amount, counterpartyName, transactionType, isOutgoing, isToOrganization, status } = getTransactionDisplayInfo(transaction); + + // Determine circle color based on transaction type + const getCircleColor = () => { + if (isOutgoing) { + return isToOrganization ? 'bg-blue-600 dark:bg-blue-600 text-white dark:text-white' : 'bg-green-600 dark:bg-green-600 text-white dark:text-white'; + } + return 'bg-green-600 dark:bg-green-600 text-white dark:text-white'; + }; + + const getStatusIcon = () => { + switch (status) { + case 'pending': + return ; + case 'failed': + return ; + default: + return ; + } + }; + + const getStatusText = () => { + switch (status) { + case 'pending': + return 'Pending'; + case 'failed': + return 'Failed'; + default: + return 'Completed'; + } + }; + + const initials = getInitials(counterpartyName); + return ( -
      -
      -
      - {displayName.charAt(0)} +
      +
      + {/* Circle with initials */} +
      + {initials}
      + + {/* Transaction details */}
      -
      -

      {displayName}

      - + {/* First line: Name and Amount */} +
      +

      {counterpartyName}

      + + {isOutgoing ? '-' : '+'} RWF {isNaN(Math.abs(amount)) ? '0' : Math.abs(amount).toLocaleString()} + +
      + + {/* Second line: Type badge, Status, and Date */} +
      +
      + {/* Transaction type badge */} + + {transactionType} + + + {/* Status indicator */} +
      + {getStatusIcon()} + {getStatusText()} +
      +
      + + {/* Date */} + {new Date(transaction.createdAt).toLocaleDateString('en-US', { month: 'numeric', day: 'numeric' })}
      -

      - {amount < 0 ? '-' : '+ '}€{isNaN(Math.abs(amount)) ? '0' : (Math.abs(amount) / 100).toFixed(2)} -

      diff --git a/components/WelcomeSection.tsx b/components/WelcomeSection.tsx index 6d07187..e021aa2 100644 --- a/components/WelcomeSection.tsx +++ b/components/WelcomeSection.tsx @@ -81,20 +81,20 @@ export const WelcomeSection: React.FC = ({ userId }) => { }; return ( -
      +
      -

      - {getGreeting()}, {userName} +

      + {getGreeting()}, {userName}

      -

      +

      Welcome back! Here's a quick overview of your QiewCode wallet.

      -

      Total Balance

      +

      Total Balance

      -

      +

      {balanceLoading ? 'Loading...' : balanceError @@ -106,7 +106,7 @@ export const WelcomeSection: React.FC = ({ userId }) => {

      -

      {getFormattedDate()}

      +

      {getFormattedDate()}

      diff --git a/components/notifications/NotificationBell.tsx b/components/notifications/NotificationBell.tsx index 495fb5e..912102c 100644 --- a/components/notifications/NotificationBell.tsx +++ b/components/notifications/NotificationBell.tsx @@ -31,7 +31,7 @@ const NotificationBell: React.FC = () => { @@ -237,7 +237,7 @@ const NotificationCenter: React.FC = ({ isOpen, onClose variant={selectedTab === 'invitations' ? 'default' : 'ghost'} size="sm" onClick={() => setSelectedTab('invitations')} - className="flex-1" + className={`flex-1 ${selectedTab !== 'invitations' ? 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-darkBg-interactive' : ''}`} > Invitations @@ -245,7 +245,7 @@ const NotificationCenter: React.FC = ({ isOpen, onClose variant={selectedTab === 'requests' ? 'default' : 'ghost'} size="sm" onClick={() => setSelectedTab('requests')} - className="flex-1" + className={`flex-1 ${selectedTab !== 'requests' ? 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-darkBg-interactive' : ''}`} > Join Requests {pendingRequestsList.length > 0 && ( @@ -258,7 +258,7 @@ const NotificationCenter: React.FC = ({ isOpen, onClose variant={selectedTab === 'contacts' ? 'default' : 'ghost'} size="sm" onClick={() => setSelectedTab('contacts')} - className="flex-1" + className={`flex-1 ${selectedTab !== 'contacts' ? 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-darkBg-interactive' : ''}`} > Contacts {pendingContactRequestsList.length > 0 && ( @@ -275,14 +275,14 @@ const NotificationCenter: React.FC = ({ isOpen, onClose {/* Show pending contact requests in contacts tab */} {selectedTab === 'contacts' && pendingContactRequestsList.length > 0 && (
      -
      +
      Pending Contact Requests
      {pendingContactRequestsList.map((request: any) => (
      = ({ isOpen, onClose
      -

      +

      {request.inviter?.firstName} {request.inviter?.lastName}

      -

      +

      {formatDistanceToNow(new Date(request.invitedAt), { addSuffix: true })}

      -

      +

      wants to add you as a contact

      -

      +

      {request.inviter?.email}

      @@ -329,7 +329,7 @@ const NotificationCenter: React.FC = ({ isOpen, onClose variant="outline" onClick={() => handleContactRequestResponse(request.id, 'decline')} disabled={isRespondingToContact || processingRequestId === request.id} - className="h-8" + className="h-8 dark:text-gray-300 dark:border-gray-600 dark:hover:bg-darkBg-card" > {processingRequestId === request.id ? ( @@ -348,14 +348,14 @@ const NotificationCenter: React.FC = ({ isOpen, onClose {/* Show pending join requests in requests tab */} {selectedTab === 'requests' && pendingRequestsList.length > 0 && (
      -
      +
      Pending Join Requests
      {pendingRequestsList.map((request: any) => (
      = ({ isOpen, onClose
      -

      +

      {request.user?.firstName} {request.user?.lastName}

      -

      +

      {formatDistanceToNow(new Date(request.createdAt), { addSuffix: true })}

      -

      +

      wants to join "{request.group?.name}"

      {request.additionalInfo && ( -

      +

      "{request.additionalInfo}"

      )} @@ -392,7 +392,7 @@ const NotificationCenter: React.FC = ({ isOpen, onClose value={rejectionReason} onChange={(e) => setRejectionReason(e.target.value)} rows={2} - className="text-xs" + className="text-xs dark:bg-darkBg-card dark:border-darkBorder-light dark:text-white" />
      @@ -449,7 +450,7 @@ const NotificationCenter: React.FC = ({ isOpen, onClose
      ))} {pendingRequestsList.length > 0 && filteredNotifications.length > 0 && ( - + )}
      )} @@ -457,15 +458,17 @@ const NotificationCenter: React.FC = ({ isOpen, onClose {/* Regular notifications */} {filteredNotifications.length === 0 && pendingRequestsList.length === 0 ? (
      - -

      No notifications yet

      + +

      No notifications yet

      ) : ( filteredNotifications.map((notification) => (
      handleNotificationClick(notification)} > @@ -474,7 +477,7 @@ const NotificationCenter: React.FC = ({ isOpen, onClose
      -

      +

      {notification.title}

      @@ -486,10 +489,10 @@ const NotificationCenter: React.FC = ({ isOpen, onClose )}
      -

      +

      {notification.message || notification.data?.message}

      -

      +

      {formatDistanceToNow(new Date(notification.createdAt), { addSuffix: true })}

      @@ -502,7 +505,7 @@ const NotificationCenter: React.FC = ({ isOpen, onClose key={index} size="sm" variant={action.type === 'accept' ? 'default' : 'outline'} - className="text-xs" + className={`text-xs ${action.type !== 'accept' ? 'dark:text-gray-300 dark:border-gray-600 dark:hover:bg-darkBg-card' : ''}`} onClick={(e) => { e.stopPropagation() if (notification.data?.userId) { diff --git a/helpers/api.ts b/helpers/api.ts index 4f27355..2c048b6 100644 --- a/helpers/api.ts +++ b/helpers/api.ts @@ -247,6 +247,8 @@ export const changePin = (currentPin: string, newPin: string) => export const resetPinAttempts = () => pinRequest('reset-attempts', {}); export const requestPinReset = (verificationMethod: 'email' | 'sms') => pinRequest('request-reset', { verificationMethod }); +export const validateResetToken = (resetToken: string) => + pinRequest('validate-reset-token', { resetToken }); export const confirmPinReset = (resetToken: string, newPin: string) => pinRequest('confirm-reset', { resetToken, newPin }); export const checkUserPinStatus = () => pinGet('status'); diff --git a/types/dashboard.ts b/types/dashboard.ts index 93b85fb..5038bf5 100644 --- a/types/dashboard.ts +++ b/types/dashboard.ts @@ -24,12 +24,34 @@ export interface Transaction { userId: string | null; organizationId: string | null; currency: string; + user?: { + id: string; + firstName: string; + lastName: string; + email: string; + } | null; + organization?: { + id: string; + name: string; + email: string; + } | null; }; receiverWallet: { id: string; userId: string | null; organizationId: string | null; currency: string; + user?: { + id: string; + firstName: string; + lastName: string; + email: string; + } | null; + organization?: { + id: string; + name: string; + email: string; + } | null; }; // Legacy fields for backward compatibility senderId?: string;