From cced901e9ae9bc93b92d06610d46623d0129a3c2 Mon Sep 17 00:00:00 2001 From: ghaddaditw <40211818-ghaddaditw@users.noreply.replit.com> Date: Fri, 30 May 2025 17:08:53 +0000 Subject: [PATCH] Set up the basic structure and core functionality for the application Initialize project with UI components, core libraries, and basic app structure. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 556aa286-edd2-4cea-8583-f4fc3cfd119b --- .gitignore | 6 + .replit | 32 + client/index.html | 13 + client/src/App.tsx | 41 + client/src/components/ai/DailyJoke.tsx | 139 + .../financial/FinancialOverview.tsx | 319 + client/src/components/layout/Header.tsx | 152 + client/src/components/layout/Sidebar.tsx | 190 + client/src/components/tasks/TaskList.tsx | 305 + client/src/components/ui/accordion.tsx | 56 + client/src/components/ui/alert-dialog.tsx | 139 + client/src/components/ui/alert.tsx | 59 + client/src/components/ui/aspect-ratio.tsx | 5 + client/src/components/ui/avatar.tsx | 50 + client/src/components/ui/badge.tsx | 36 + client/src/components/ui/breadcrumb.tsx | 115 + client/src/components/ui/button.tsx | 56 + client/src/components/ui/calendar.tsx | 68 + client/src/components/ui/card.tsx | 79 + client/src/components/ui/carousel.tsx | 260 + client/src/components/ui/chart.tsx | 365 + client/src/components/ui/checkbox.tsx | 28 + client/src/components/ui/collapsible.tsx | 11 + client/src/components/ui/command.tsx | 151 + client/src/components/ui/context-menu.tsx | 198 + client/src/components/ui/dialog.tsx | 122 + client/src/components/ui/drawer.tsx | 118 + client/src/components/ui/dropdown-menu.tsx | 198 + client/src/components/ui/form.tsx | 178 + client/src/components/ui/hover-card.tsx | 29 + client/src/components/ui/input-otp.tsx | 69 + client/src/components/ui/input.tsx | 22 + client/src/components/ui/label.tsx | 24 + client/src/components/ui/menubar.tsx | 256 + client/src/components/ui/navigation-menu.tsx | 128 + client/src/components/ui/pagination.tsx | 117 + client/src/components/ui/popover.tsx | 29 + client/src/components/ui/progress.tsx | 28 + client/src/components/ui/radio-group.tsx | 42 + client/src/components/ui/resizable.tsx | 45 + client/src/components/ui/scroll-area.tsx | 46 + client/src/components/ui/select.tsx | 160 + client/src/components/ui/separator.tsx | 29 + client/src/components/ui/sheet.tsx | 140 + client/src/components/ui/sidebar.tsx | 771 ++ client/src/components/ui/skeleton.tsx | 15 + client/src/components/ui/slider.tsx | 26 + client/src/components/ui/switch.tsx | 27 + client/src/components/ui/table.tsx | 117 + client/src/components/ui/tabs.tsx | 53 + client/src/components/ui/textarea.tsx | 22 + client/src/components/ui/toast.tsx | 127 + client/src/components/ui/toaster.tsx | 33 + client/src/components/ui/toggle-group.tsx | 61 + client/src/components/ui/toggle.tsx | 43 + client/src/components/ui/tooltip.tsx | 30 + .../src/components/voice/VoiceIndicator.tsx | 118 + client/src/components/voice/VoiceRecorder.tsx | 265 + client/src/context/AuthContext.tsx | 86 + client/src/context/ThemeContext.tsx | 47 + client/src/hooks/use-mobile.tsx | 19 + client/src/hooks/use-toast.ts | 191 + client/src/hooks/useAuth.ts | 10 + client/src/hooks/useVoice.ts | 192 + client/src/index.css | 213 + client/src/lib/queryClient.ts | 57 + client/src/lib/utils.ts | 6 + client/src/lib/voiceProcessor.ts | 176 + client/src/main.tsx | 5 + client/src/pages/DashboardPage.tsx | 253 + client/src/pages/LandingPage.tsx | 335 + client/src/pages/LoginPage.tsx | 254 + client/src/pages/not-found.tsx | 21 + client/src/services/aiService.ts | 70 + client/src/services/authService.ts | 56 + client/src/services/financialService.ts | 60 + client/src/services/taskService.ts | 43 + client/src/services/voiceService.ts | 50 + components.json | 20 + drizzle.config.ts | 14 + package-lock.json | 9284 +++++++++++++++++ package.json | 108 + postcss.config.js | 6 + server/controllers/aiController.ts | 160 + server/controllers/authController.ts | 185 + server/controllers/financialController.ts | 181 + server/controllers/taskController.ts | 150 + server/controllers/voiceController.ts | 203 + server/db.ts | 15 + server/index.ts | 70 + server/middleware/auth.ts | 52 + server/routes.ts | 136 + server/services/aiService.ts | 336 + server/services/voiceService.ts | 330 + server/storage.ts | 286 + server/vite.ts | 85 + shared/schema.ts | 218 + tailwind.config.ts | 90 + tsconfig.json | 23 + vite.config.ts | 31 + 100 files changed, 20488 insertions(+) create mode 100644 .gitignore create mode 100644 client/index.html create mode 100644 client/src/App.tsx create mode 100644 client/src/components/ai/DailyJoke.tsx create mode 100644 client/src/components/financial/FinancialOverview.tsx create mode 100644 client/src/components/layout/Header.tsx create mode 100644 client/src/components/layout/Sidebar.tsx create mode 100644 client/src/components/tasks/TaskList.tsx create mode 100644 client/src/components/ui/accordion.tsx create mode 100644 client/src/components/ui/alert-dialog.tsx create mode 100644 client/src/components/ui/alert.tsx create mode 100644 client/src/components/ui/aspect-ratio.tsx create mode 100644 client/src/components/ui/avatar.tsx create mode 100644 client/src/components/ui/badge.tsx create mode 100644 client/src/components/ui/breadcrumb.tsx create mode 100644 client/src/components/ui/button.tsx create mode 100644 client/src/components/ui/calendar.tsx create mode 100644 client/src/components/ui/card.tsx create mode 100644 client/src/components/ui/carousel.tsx create mode 100644 client/src/components/ui/chart.tsx create mode 100644 client/src/components/ui/checkbox.tsx create mode 100644 client/src/components/ui/collapsible.tsx create mode 100644 client/src/components/ui/command.tsx create mode 100644 client/src/components/ui/context-menu.tsx create mode 100644 client/src/components/ui/dialog.tsx create mode 100644 client/src/components/ui/drawer.tsx create mode 100644 client/src/components/ui/dropdown-menu.tsx create mode 100644 client/src/components/ui/form.tsx create mode 100644 client/src/components/ui/hover-card.tsx create mode 100644 client/src/components/ui/input-otp.tsx create mode 100644 client/src/components/ui/input.tsx create mode 100644 client/src/components/ui/label.tsx create mode 100644 client/src/components/ui/menubar.tsx create mode 100644 client/src/components/ui/navigation-menu.tsx create mode 100644 client/src/components/ui/pagination.tsx create mode 100644 client/src/components/ui/popover.tsx create mode 100644 client/src/components/ui/progress.tsx create mode 100644 client/src/components/ui/radio-group.tsx create mode 100644 client/src/components/ui/resizable.tsx create mode 100644 client/src/components/ui/scroll-area.tsx create mode 100644 client/src/components/ui/select.tsx create mode 100644 client/src/components/ui/separator.tsx create mode 100644 client/src/components/ui/sheet.tsx create mode 100644 client/src/components/ui/sidebar.tsx create mode 100644 client/src/components/ui/skeleton.tsx create mode 100644 client/src/components/ui/slider.tsx create mode 100644 client/src/components/ui/switch.tsx create mode 100644 client/src/components/ui/table.tsx create mode 100644 client/src/components/ui/tabs.tsx create mode 100644 client/src/components/ui/textarea.tsx create mode 100644 client/src/components/ui/toast.tsx create mode 100644 client/src/components/ui/toaster.tsx create mode 100644 client/src/components/ui/toggle-group.tsx create mode 100644 client/src/components/ui/toggle.tsx create mode 100644 client/src/components/ui/tooltip.tsx create mode 100644 client/src/components/voice/VoiceIndicator.tsx create mode 100644 client/src/components/voice/VoiceRecorder.tsx create mode 100644 client/src/context/AuthContext.tsx create mode 100644 client/src/context/ThemeContext.tsx create mode 100644 client/src/hooks/use-mobile.tsx create mode 100644 client/src/hooks/use-toast.ts create mode 100644 client/src/hooks/useAuth.ts create mode 100644 client/src/hooks/useVoice.ts create mode 100644 client/src/index.css create mode 100644 client/src/lib/queryClient.ts create mode 100644 client/src/lib/utils.ts create mode 100644 client/src/lib/voiceProcessor.ts create mode 100644 client/src/main.tsx create mode 100644 client/src/pages/DashboardPage.tsx create mode 100644 client/src/pages/LandingPage.tsx create mode 100644 client/src/pages/LoginPage.tsx create mode 100644 client/src/pages/not-found.tsx create mode 100644 client/src/services/aiService.ts create mode 100644 client/src/services/authService.ts create mode 100644 client/src/services/financialService.ts create mode 100644 client/src/services/taskService.ts create mode 100644 client/src/services/voiceService.ts create mode 100644 components.json create mode 100644 drizzle.config.ts create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 postcss.config.js create mode 100644 server/controllers/aiController.ts create mode 100644 server/controllers/authController.ts create mode 100644 server/controllers/financialController.ts create mode 100644 server/controllers/taskController.ts create mode 100644 server/controllers/voiceController.ts create mode 100644 server/db.ts create mode 100644 server/index.ts create mode 100644 server/middleware/auth.ts create mode 100644 server/routes.ts create mode 100644 server/services/aiService.ts create mode 100644 server/services/voiceService.ts create mode 100644 server/storage.ts create mode 100644 server/vite.ts create mode 100644 shared/schema.ts create mode 100644 tailwind.config.ts create mode 100644 tsconfig.json create mode 100644 vite.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f9ba7f8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +node_modules +dist +.DS_Store +server/public +vite.config.ts.* +*.tar.gz \ No newline at end of file diff --git a/.replit b/.replit index e69de29..b5f2185 100644 --- a/.replit +++ b/.replit @@ -0,0 +1,32 @@ +modules = ["nodejs-20", "web", "postgresql-16"] +run = "npm run dev" +hidden = [".config", ".git", "generated-icon.png", "node_modules", "dist"] + +[nix] +channel = "stable-24_05" + +[deployment] +deploymentTarget = "autoscale" +build = ["npm", "run", "build"] +run = ["npm", "run", "start"] + +[workflows] +runButton = "Project" + +[[workflows.workflow]] +name = "Project" +mode = "parallel" +author = "agent" + +[[workflows.workflow.tasks]] +task = "workflow.run" +args = "Start application" + +[[workflows.workflow]] +name = "Start application" +author = "agent" + +[[workflows.workflow.tasks]] +task = "shell.exec" +args = "npm run dev" +waitForPort = 5000 diff --git a/client/index.html b/client/index.html new file mode 100644 index 0000000..4b4d09e --- /dev/null +++ b/client/index.html @@ -0,0 +1,13 @@ + + + + + + + +
+ + + + + \ No newline at end of file diff --git a/client/src/App.tsx b/client/src/App.tsx new file mode 100644 index 0000000..425281c --- /dev/null +++ b/client/src/App.tsx @@ -0,0 +1,41 @@ +import { Switch, Route } from "wouter"; +import { queryClient } from "./lib/queryClient"; +import { QueryClientProvider } from "@tanstack/react-query"; +import { Toaster } from "@/components/ui/toaster"; +import { TooltipProvider } from "@/components/ui/tooltip"; +import { AuthProvider } from "@/context/AuthContext"; +import { ThemeProvider } from "@/context/ThemeContext"; +import LandingPage from "@/pages/LandingPage"; +import DashboardPage from "@/pages/DashboardPage"; +import LoginPage from "@/pages/LoginPage"; +import OnboardingPage from "@/pages/OnboardingPage"; +import NotFound from "@/pages/not-found"; + +function Router() { + return ( + + + + + + + + ); +} + +function App() { + return ( + + + + + + + + + + + ); +} + +export default App; diff --git a/client/src/components/ai/DailyJoke.tsx b/client/src/components/ai/DailyJoke.tsx new file mode 100644 index 0000000..a39396a --- /dev/null +++ b/client/src/components/ai/DailyJoke.tsx @@ -0,0 +1,139 @@ +import { useQuery } from "@tanstack/react-query"; +import { Card, CardContent } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { useVoice } from "@/hooks/useVoice"; +import { aiService } from "@/services/aiService"; +import { Brain, Volume2, RefreshCw } from "lucide-react"; +import { Skeleton } from "@/components/ui/skeleton"; + +export default function DailyJoke() { + const { speak } = useVoice(); + + const { + data: jokeData, + isLoading, + error, + refetch + } = useQuery({ + queryKey: ["/api/ai/daily-joke"], + staleTime: 30 * 60 * 1000, // Cache for 30 minutes + retry: 2, + }); + + const handlePlayJoke = () => { + if (jokeData?.joke) { + speak(jokeData.joke); + } + }; + + const handleRefreshJoke = () => { + refetch(); + }; + + if (isLoading) { + return ( + + +
+
+ +
+
+ + + + +
+
+
+
+ ); + } + + if (error) { + return ( + + +
+
+ +
+
+

AI Joke Unavailable

+

+ Failed to load today's AI joke. Please try again. +

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

Daily AI Joke

+
+ + +
+
+ +

+ {jokeData?.joke || "Loading today's AI-generated joke..."} +

+ +
+
+ + {jokeData?.cached ? "Cached" : "Fresh"} + + Powered by Local AI +
+ + {jokeData?.timestamp && ( + + {new Date(jokeData.timestamp).toLocaleDateString()} + + )} +
+ + {jokeData?.processingTime && ( +
+ Generated in {jokeData.processingTime}ms +
+ )} +
+
+
+
+ ); +} diff --git a/client/src/components/financial/FinancialOverview.tsx b/client/src/components/financial/FinancialOverview.tsx new file mode 100644 index 0000000..3e7a67c --- /dev/null +++ b/client/src/components/financial/FinancialOverview.tsx @@ -0,0 +1,319 @@ +import { useQuery } from "@tanstack/react-query"; +import { financialService } from "@/services/financialService"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Skeleton } from "@/components/ui/skeleton"; +import { + DollarSign, + TrendingUp, + TrendingDown, + Wallet, + ArrowUpRight, + ArrowDownRight, + Mic, + Calendar +} from "lucide-react"; +import { cn } from "@/lib/utils"; +import { formatDistanceToNow } from "date-fns"; + +interface FinancialOverviewProps { + period?: "week" | "month" | "year"; + limit?: number; + showHeader?: boolean; + className?: string; +} + +export default function FinancialOverview({ + period = "month", + limit = 5, + showHeader = true, + className +}: FinancialOverviewProps) { + const { + data: summaryData, + isLoading: summaryLoading + } = useQuery({ + queryKey: ["/api/financial/summary", { period }], + staleTime: 60 * 1000, + }); + + const { + data: recordsData, + isLoading: recordsLoading + } = useQuery({ + queryKey: ["/api/financial/records"], + staleTime: 30 * 1000, + }); + + const isLoading = summaryLoading || recordsLoading; + + const formatCurrency = (amount: number) => { + return new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', + minimumFractionDigits: 2, + }).format(amount); + }; + + const getChangeColor = (value: number) => { + if (value > 0) return "text-green-600 dark:text-green-400"; + if (value < 0) return "text-red-600 dark:text-red-400"; + return "text-muted-foreground"; + }; + + const getTransactionIcon = (type: string, amount: number) => { + if (type === "income") { + return ; + } + return ; + }; + + const getCategoryColor = (category: string, type: string) => { + if (type === "income") { + return "bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-400"; + } + + const categoryColors: Record = { + "food": "bg-orange-100 text-orange-800 dark:bg-orange-900/20 dark:text-orange-400", + "transport": "bg-blue-100 text-blue-800 dark:bg-blue-900/20 dark:text-blue-400", + "entertainment": "bg-purple-100 text-purple-800 dark:bg-purple-900/20 dark:text-purple-400", + "utilities": "bg-gray-100 text-gray-800 dark:bg-gray-900/20 dark:text-gray-400", + "healthcare": "bg-pink-100 text-pink-800 dark:bg-pink-900/20 dark:text-pink-400", + "shopping": "bg-indigo-100 text-indigo-800 dark:bg-indigo-900/20 dark:text-indigo-400", + }; + + return categoryColors[category.toLowerCase()] || "bg-slate-100 text-slate-800 dark:bg-slate-900/20 dark:text-slate-400"; + }; + + if (isLoading) { + return ( +
+ {/* Summary Cards Skeleton */} +
+ {Array.from({ length: 3 }).map((_, i) => ( + + +
+
+ + +
+ +
+
+ +
+
+
+ ))} +
+ + {/* Recent Transactions Skeleton */} + + + + + +
+ {Array.from({ length: 3 }).map((_, i) => ( +
+ +
+ + +
+ +
+ ))} +
+
+
+
+ ); + } + + const summary = summaryData?.summary || { income: 0, expenses: 0, net: 0 }; + const records = recordsData?.records || []; + const recentRecords = records.slice(0, limit); + + return ( +
+ {/* Financial Summary Cards */} +
+ {/* Income Card */} + + +
+
+

Income

+

+ {formatCurrency(summary.income)} +

+
+
+ +
+
+
+ + This {period} + +
+
+
+ + {/* Expenses Card */} + + +
+
+

Expenses

+

+ {formatCurrency(summary.expenses)} +

+
+
+ +
+
+
+ + This {period} + +
+
+
+ + {/* Net Balance Card */} + + +
+
+

Net Balance

+

+ {formatCurrency(summary.net)} +

+
+
= 0 + ? "bg-primary-100 dark:bg-primary-900/50" + : "bg-red-100 dark:bg-red-900/50" + )}> + = 0 + ? "text-primary-600 dark:text-primary-400" + : "text-red-600 dark:text-red-400" + )} /> +
+
+
+ {summary.net >= 0 ? ( + + Positive + + ) : ( + + Deficit + + )} +
+
+
+
+ + {/* Recent Transactions */} + + {showHeader && ( + +
+ + + Recent Transactions + + +
+
+ )} + + + {recentRecords.length === 0 ? ( +
+ +

No financial records found

+

Add transactions using voice commands or manually

+
+ ) : ( +
+ {recentRecords.map((record) => ( +
+
+
+ {getTransactionIcon(record.type, parseFloat(record.amount))} +
+ +
+
+

+ {record.description || record.category} +

+ {record.createdViaVoice && ( +
+ + Voice +
+ )} +
+ +
+ + {record.category} + + +
+ + + {formatDistanceToNow(new Date(record.date), { addSuffix: true })} + +
+
+
+
+ +
+

+ {record.type === "income" ? "+" : "-"}{formatCurrency(parseFloat(record.amount))} +

+
+
+ ))} +
+ )} +
+
+
+ ); +} diff --git a/client/src/components/layout/Header.tsx b/client/src/components/layout/Header.tsx new file mode 100644 index 0000000..27c8a1d --- /dev/null +++ b/client/src/components/layout/Header.tsx @@ -0,0 +1,152 @@ +import { Link, useLocation } from "wouter"; +import { useAuth } from "@/hooks/useAuth"; +import { useTheme } from "@/context/ThemeContext"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Avatar, AvatarFallback } from "@/components/ui/avatar"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { + Moon, + Sun, + Mic, + User, + Settings, + LogOut, + Activity +} from "lucide-react"; + +export default function Header() { + const { user, logout } = useAuth(); + const { theme, toggleTheme } = useTheme(); + const [, setLocation] = useLocation(); + + const handleLogout = async () => { + await logout(); + setLocation("/"); + }; + + const getUserInitials = (user: any) => { + if (user.firstName && user.lastName) { + return `${user.firstName[0]}${user.lastName[0]}`.toUpperCase(); + } + return user.username?.[0]?.toUpperCase() || "U"; + }; + + return ( +
+
+ {/* Logo and Brand */} +
+ +
+ +
+ + TaskFin + + +
+ + {/* Navigation */} + {user && ( + + )} + + {/* Right side actions */} +
+
+ + {/* Voice Status Indicator */} +
+ + Voice Ready +
+ + {/* Theme Toggle */} + + + {/* User Menu or Login */} + {user ? ( + + + + + + +
+

+ {user.firstName && user.lastName + ? `${user.firstName} ${user.lastName}` + : user.username + } +

+

+ {user.email} +

+
+ + {user.role === "admin" ? "Admin" : user.role === "pro" ? "Pro" : "Standard"} + +
+
+
+ + setLocation("/settings")}> + + Settings + + setLocation("/profile")}> + + Profile + + + + + Log out + +
+
+ ) : ( +
+ + +
+ )} +
+
+
+ ); +} diff --git a/client/src/components/layout/Sidebar.tsx b/client/src/components/layout/Sidebar.tsx new file mode 100644 index 0000000..6e52de1 --- /dev/null +++ b/client/src/components/layout/Sidebar.tsx @@ -0,0 +1,190 @@ +import { Link, useLocation } from "wouter"; +import { useAuth } from "@/hooks/useAuth"; +import { cn } from "@/lib/utils"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Separator } from "@/components/ui/separator"; +import { + Calendar, + DollarSign, + LayoutDashboard, + Mic, + Brain, + Settings, + Users, + BarChart3, + Shield, + Zap +} from "lucide-react"; + +interface SidebarProps { + className?: string; +} + +const navigation = [ + { + name: "Dashboard", + href: "/dashboard", + icon: LayoutDashboard, + }, + { + name: "Tasks", + href: "/tasks", + icon: Calendar, + badge: "3", // This would come from task count + }, + { + name: "Finances", + href: "/finances", + icon: DollarSign, + }, + { + name: "Voice Commands", + href: "/voice", + icon: Mic, + }, + { + name: "AI Assistant", + href: "/ai", + icon: Brain, + }, +]; + +const managementNavigation = [ + { + name: "Analytics", + href: "/analytics", + icon: BarChart3, + roles: ["pro", "admin"], + }, + { + name: "Settings", + href: "/settings", + icon: Settings, + }, +]; + +const adminNavigation = [ + { + name: "User Management", + href: "/admin/users", + icon: Users, + roles: ["admin"], + }, + { + name: "System Status", + href: "/admin/system", + icon: Shield, + roles: ["admin"], + }, + { + name: "Model Management", + href: "/admin/models", + icon: Zap, + roles: ["admin"], + }, +]; + +export default function Sidebar({ className }: SidebarProps) { + const [location] = useLocation(); + const { user } = useAuth(); + + const isActive = (href: string) => { + if (href === "/dashboard") { + return location === "/" || location === "/dashboard"; + } + return location.startsWith(href); + }; + + const canAccess = (roles?: string[]) => { + if (!roles) return true; + return user && roles.includes(user.role); + }; + + return ( +
+
+
+
+

+ Navigation +

+
+ {navigation.map((item) => ( + + + + ))} +
+
+
+ + + +
+

+ Management +

+
+ {managementNavigation + .filter((item) => canAccess(item.roles)) + .map((item) => ( + + + + ))} +
+
+ + {user?.role === "admin" && ( + <> + +
+

+ Administration +

+
+ {adminNavigation + .filter((item) => canAccess(item.roles)) + .map((item) => ( + + + + ))} +
+
+ + )} +
+
+ ); +} diff --git a/client/src/components/tasks/TaskList.tsx b/client/src/components/tasks/TaskList.tsx new file mode 100644 index 0000000..ab1cec4 --- /dev/null +++ b/client/src/components/tasks/TaskList.tsx @@ -0,0 +1,305 @@ +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { taskService } from "@/services/taskService"; +import { useToast } from "@/hooks/use-toast"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Skeleton } from "@/components/ui/skeleton"; +import { + Calendar, + Clock, + CheckCircle2, + Circle, + Mic, + Plus, + MoreHorizontal +} from "lucide-react"; +import { cn } from "@/lib/utils"; +import { formatDistanceToNow, isAfter, isBefore, startOfDay } from "date-fns"; + +interface TaskListProps { + status?: string; + limit?: number; + showHeader?: boolean; + className?: string; +} + +export default function TaskList({ + status, + limit, + showHeader = true, + className +}: TaskListProps) { + const { toast } = useToast(); + const queryClient = useQueryClient(); + + const { + data: tasksData, + isLoading, + error + } = useQuery({ + queryKey: ["/api/tasks", { status }], + staleTime: 30 * 1000, + }); + + const completeTaskMutation = useMutation({ + mutationFn: (taskId: number) => taskService.completeTask(taskId), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["/api/tasks"] }); + toast({ + title: "Task completed", + description: "Task marked as completed successfully", + }); + }, + onError: (error) => { + toast({ + title: "Error", + description: "Failed to complete task", + variant: "destructive", + }); + }, + }); + + const deleteTaskMutation = useMutation({ + mutationFn: (taskId: number) => taskService.deleteTask(taskId), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["/api/tasks"] }); + toast({ + title: "Task deleted", + description: "Task deleted successfully", + }); + }, + onError: (error) => { + toast({ + title: "Error", + description: "Failed to delete task", + variant: "destructive", + }); + }, + }); + + const handleCompleteTask = (taskId: number) => { + completeTaskMutation.mutate(taskId); + }; + + const getPriorityColor = (priority: string) => { + switch (priority) { + case "high": + return "bg-red-100 text-red-800 dark:bg-red-900/20 dark:text-red-400"; + case "medium": + return "bg-yellow-100 text-yellow-800 dark:bg-yellow-900/20 dark:text-yellow-400"; + case "low": + return "bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-400"; + default: + return "bg-gray-100 text-gray-800 dark:bg-gray-900/20 dark:text-gray-400"; + } + }; + + const getTaskStatus = (task: any) => { + if (task.status === "completed") return "completed"; + if (!task.dueDate) return "normal"; + + const now = new Date(); + const dueDate = new Date(task.dueDate); + const today = startOfDay(now); + const taskDueDay = startOfDay(dueDate); + + if (isBefore(taskDueDay, today)) return "overdue"; + if (taskDueDay.getTime() === today.getTime()) return "due-today"; + return "normal"; + }; + + const getStatusBadge = (task: any) => { + const status = getTaskStatus(task); + + switch (status) { + case "completed": + return Completed; + case "overdue": + return Overdue; + case "due-today": + return Due Today; + default: + return null; + } + }; + + if (isLoading) { + return ( + + {showHeader && ( + + + + Tasks + + + )} + +
+ {Array.from({ length: 3 }).map((_, i) => ( +
+ +
+ + +
+ +
+ ))} +
+
+
+ ); + } + + if (error) { + return ( + + {showHeader && ( + + + + Tasks + + + )} + +
+ +

Failed to load tasks

+
+
+
+ ); + } + + const tasks = tasksData?.tasks || []; + const displayTasks = limit ? tasks.slice(0, limit) : tasks; + + if (displayTasks.length === 0) { + return ( + + {showHeader && ( + + + + Tasks + + + )} + +
+ +

No tasks found

+

Create a task using voice commands or the dashboard

+
+
+
+ ); + } + + return ( + + {showHeader && ( + +
+ + + Tasks + {tasks.length} + + +
+
+ )} + + +
+ {displayTasks.map((task) => ( +
+ {/* Completion Checkbox */} +
+ handleCompleteTask(task.id)} + disabled={completeTaskMutation.isPending} + className="w-5 h-5" + /> +
+ + {/* Task Content */} +
+
+
+

+ {task.title} +

+ + {task.description && ( +

+ {task.description} +

+ )} + +
+ {/* Priority Badge */} + + {task.priority} + + + {/* Voice Created Indicator */} + {task.createdViaVoice && ( +
+ + Voice +
+ )} + + {/* Due Date */} + {task.dueDate && ( +
+ + + {formatDistanceToNow(new Date(task.dueDate), { addSuffix: true })} + +
+ )} +
+
+ + {/* Status Badge */} +
+ {getStatusBadge(task)} +
+
+
+
+ ))} +
+ + {limit && tasks.length > limit && ( +
+ +
+ )} +
+
+ ); +} diff --git a/client/src/components/ui/accordion.tsx b/client/src/components/ui/accordion.tsx new file mode 100644 index 0000000..e6a723d --- /dev/null +++ b/client/src/components/ui/accordion.tsx @@ -0,0 +1,56 @@ +import * as React from "react" +import * as AccordionPrimitive from "@radix-ui/react-accordion" +import { ChevronDown } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Accordion = AccordionPrimitive.Root + +const AccordionItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AccordionItem.displayName = "AccordionItem" + +const AccordionTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + svg]:rotate-180", + className + )} + {...props} + > + {children} + + + +)) +AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName + +const AccordionContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + +
{children}
+
+)) + +AccordionContent.displayName = AccordionPrimitive.Content.displayName + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/client/src/components/ui/alert-dialog.tsx b/client/src/components/ui/alert-dialog.tsx new file mode 100644 index 0000000..8722561 --- /dev/null +++ b/client/src/components/ui/alert-dialog.tsx @@ -0,0 +1,139 @@ +import * as React from "react" +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" + +import { cn } from "@/lib/utils" +import { buttonVariants } from "@/components/ui/button" + +const AlertDialog = AlertDialogPrimitive.Root + +const AlertDialogTrigger = AlertDialogPrimitive.Trigger + +const AlertDialogPortal = AlertDialogPrimitive.Portal + +const AlertDialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName + +const AlertDialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + +)) +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName + +const AlertDialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +AlertDialogHeader.displayName = "AlertDialogHeader" + +const AlertDialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +AlertDialogFooter.displayName = "AlertDialogFooter" + +const AlertDialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName + +const AlertDialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogDescription.displayName = + AlertDialogPrimitive.Description.displayName + +const AlertDialogAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName + +const AlertDialogCancel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +} diff --git a/client/src/components/ui/alert.tsx b/client/src/components/ui/alert.tsx new file mode 100644 index 0000000..41fa7e0 --- /dev/null +++ b/client/src/components/ui/alert.tsx @@ -0,0 +1,59 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const alertVariants = cva( + "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", + { + variants: { + variant: { + default: "bg-background text-foreground", + destructive: + "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +const Alert = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & VariantProps +>(({ className, variant, ...props }, ref) => ( +
+)) +Alert.displayName = "Alert" + +const AlertTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertTitle.displayName = "AlertTitle" + +const AlertDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertDescription.displayName = "AlertDescription" + +export { Alert, AlertTitle, AlertDescription } diff --git a/client/src/components/ui/aspect-ratio.tsx b/client/src/components/ui/aspect-ratio.tsx new file mode 100644 index 0000000..c4abbf3 --- /dev/null +++ b/client/src/components/ui/aspect-ratio.tsx @@ -0,0 +1,5 @@ +import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio" + +const AspectRatio = AspectRatioPrimitive.Root + +export { AspectRatio } diff --git a/client/src/components/ui/avatar.tsx b/client/src/components/ui/avatar.tsx new file mode 100644 index 0000000..51e507b --- /dev/null +++ b/client/src/components/ui/avatar.tsx @@ -0,0 +1,50 @@ +"use client" + +import * as React from "react" +import * as AvatarPrimitive from "@radix-ui/react-avatar" + +import { cn } from "@/lib/utils" + +const Avatar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Avatar.displayName = AvatarPrimitive.Root.displayName + +const AvatarImage = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarImage.displayName = AvatarPrimitive.Image.displayName + +const AvatarFallback = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/client/src/components/ui/badge.tsx b/client/src/components/ui/badge.tsx new file mode 100644 index 0000000..f000e3e --- /dev/null +++ b/client/src/components/ui/badge.tsx @@ -0,0 +1,36 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", + secondary: + "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", + outline: "text-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ) +} + +export { Badge, badgeVariants } diff --git a/client/src/components/ui/breadcrumb.tsx b/client/src/components/ui/breadcrumb.tsx new file mode 100644 index 0000000..60e6c96 --- /dev/null +++ b/client/src/components/ui/breadcrumb.tsx @@ -0,0 +1,115 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { ChevronRight, MoreHorizontal } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Breadcrumb = React.forwardRef< + HTMLElement, + React.ComponentPropsWithoutRef<"nav"> & { + separator?: React.ReactNode + } +>(({ ...props }, ref) =>