From caace928ac81c284629ee50942d72179d4da9784 Mon Sep 17 00:00:00 2001 From: Biswa Kalyan Bhuyan Date: Thu, 24 Apr 2025 09:13:07 +0530 Subject: feat: Fix loan API type assertion and complete core loan features - Resolve interface conversion panic in loan handlers by correcting user type assertions from *models.User to models.User - Finalize loan management API integration with frontend components - Implement remaining loan calculation logic and CRUD operations - Connect loan display components to backend APIs as per Phase 3 - Update project status in README.md to reflect completed loan features - Add CORS middleware configuration for frontend-backend communication This commit completes core loan management functionality and fixes critical type safety issues in the API handlers, enabling proper user context handling. --- frontend/src/components/shared/Notification.tsx | 82 ++++++++++ .../src/components/shared/NotificationContext.tsx | 55 +++++++ frontend/src/components/shared/ThemeProvider.tsx | 56 +++++++ frontend/src/components/shared/ThemeToggle.tsx | 36 +++++ frontend/src/components/ui/button.tsx | 59 ++++++++ frontend/src/components/ui/card.tsx | 92 ++++++++++++ frontend/src/components/ui/dialog.tsx | 135 +++++++++++++++++ frontend/src/components/ui/form.tsx | 167 +++++++++++++++++++++ frontend/src/components/ui/input.tsx | 21 +++ frontend/src/components/ui/label.tsx | 24 +++ frontend/src/components/ui/table.tsx | 116 ++++++++++++++ 11 files changed, 843 insertions(+) create mode 100644 frontend/src/components/shared/Notification.tsx create mode 100644 frontend/src/components/shared/NotificationContext.tsx create mode 100644 frontend/src/components/shared/ThemeProvider.tsx create mode 100644 frontend/src/components/shared/ThemeToggle.tsx create mode 100644 frontend/src/components/ui/button.tsx create mode 100644 frontend/src/components/ui/card.tsx create mode 100644 frontend/src/components/ui/dialog.tsx create mode 100644 frontend/src/components/ui/form.tsx create mode 100644 frontend/src/components/ui/input.tsx create mode 100644 frontend/src/components/ui/label.tsx create mode 100644 frontend/src/components/ui/table.tsx (limited to 'frontend/src/components') diff --git a/frontend/src/components/shared/Notification.tsx b/frontend/src/components/shared/Notification.tsx new file mode 100644 index 0000000..bcc11c4 --- /dev/null +++ b/frontend/src/components/shared/Notification.tsx @@ -0,0 +1,82 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { CheckCircle, AlertCircle, X } from 'lucide-react'; + +export type NotificationType = 'success' | 'error' | 'info'; + +interface NotificationProps { + type: NotificationType; + message: string; + duration?: number; + onClose?: () => void; +} + +export function Notification({ + type, + message, + duration = 5000, // Default duration of 5 seconds + onClose +}: NotificationProps) { + const [isVisible, setIsVisible] = useState(true); + + useEffect(() => { + if (duration > 0) { + const timer = setTimeout(() => { + setIsVisible(false); + if (onClose) onClose(); + }, duration); + + return () => clearTimeout(timer); + } + }, [duration, onClose]); + + const handleClose = () => { + setIsVisible(false); + if (onClose) onClose(); + }; + + if (!isVisible) return null; + + const getIcon = () => { + switch (type) { + case 'success': + return ; + case 'error': + return ; + case 'info': + return ; + default: + return null; + } + }; + + const getContainerClasses = () => { + const baseClasses = 'fixed top-4 right-4 z-50 flex items-center gap-3 rounded-lg p-4 shadow-md max-w-sm'; + + switch (type) { + case 'success': + return `${baseClasses} bg-green-50 text-green-800 border border-green-200`; + case 'error': + return `${baseClasses} bg-red-50 text-red-800 border border-red-200`; + case 'info': + return `${baseClasses} bg-blue-50 text-blue-800 border border-blue-200`; + default: + return baseClasses; + } + }; + + return ( +
+ {getIcon()} +
{message}
+ +
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/shared/NotificationContext.tsx b/frontend/src/components/shared/NotificationContext.tsx new file mode 100644 index 0000000..a8f6454 --- /dev/null +++ b/frontend/src/components/shared/NotificationContext.tsx @@ -0,0 +1,55 @@ +'use client'; + +import { createContext, useContext, useState, ReactNode } from 'react'; +import { Notification, NotificationType } from './Notification'; + +type NotificationData = { + id: string; + type: NotificationType; + message: string; + duration?: number; +}; + +interface NotificationContextType { + showNotification: (type: NotificationType, message: string, duration?: number) => void; + hideNotification: (id: string) => void; +} + +const NotificationContext = createContext(undefined); + +export function NotificationProvider({ children }: { children: ReactNode }) { + const [notifications, setNotifications] = useState([]); + + const showNotification = (type: NotificationType, message: string, duration?: number) => { + const id = Math.random().toString(36).substring(2, 9); + setNotifications(prev => [...prev, { id, type, message, duration }]); + return id; + }; + + const hideNotification = (id: string) => { + setNotifications(prev => prev.filter(notification => notification.id !== id)); + }; + + return ( + + {children} + {notifications.map(notification => ( + hideNotification(notification.id)} + /> + ))} + + ); +} + +export function useNotification() { + const context = useContext(NotificationContext); + if (context === undefined) { + throw new Error('useNotification must be used within a NotificationProvider'); + } + return context; +} \ No newline at end of file diff --git a/frontend/src/components/shared/ThemeProvider.tsx b/frontend/src/components/shared/ThemeProvider.tsx new file mode 100644 index 0000000..2fc67d9 --- /dev/null +++ b/frontend/src/components/shared/ThemeProvider.tsx @@ -0,0 +1,56 @@ +'use client'; + +import { createContext, useContext, useEffect, useState, ReactNode } from 'react'; + +type Theme = 'light' | 'dark' | 'system'; + +interface ThemeContextType { + theme: Theme; + setTheme: (theme: Theme) => void; +} + +const ThemeContext = createContext(undefined); + +export function ThemeProvider({ children }: { children: ReactNode }) { + const [theme, setTheme] = useState('system'); + + useEffect(() => { + // Get the theme preference from localStorage if available + const storedTheme = localStorage.getItem('theme') as Theme | null; + if (storedTheme) { + setTheme(storedTheme); + } + }, []); + + useEffect(() => { + const root = window.document.documentElement; + + // Remove all theme classes + root.classList.remove('light', 'dark'); + + // Add the appropriate class based on theme + if (theme === 'system') { + const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; + root.classList.add(systemTheme); + } else { + root.classList.add(theme); + } + + // Store the preference + localStorage.setItem('theme', theme); + }, [theme]); + + return ( + + {children} + + ); +} + +export function useTheme() { + const context = useContext(ThemeContext); + if (context === undefined) { + throw new Error('useTheme must be used within a ThemeProvider'); + } + return context; +} \ No newline at end of file diff --git a/frontend/src/components/shared/ThemeToggle.tsx b/frontend/src/components/shared/ThemeToggle.tsx new file mode 100644 index 0000000..679bbc5 --- /dev/null +++ b/frontend/src/components/shared/ThemeToggle.tsx @@ -0,0 +1,36 @@ +'use client'; + +import { Moon, Sun } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { useTheme } from './ThemeProvider'; + +export function ThemeToggle() { + const { theme, setTheme } = useTheme(); + + const toggleTheme = () => { + if (theme === 'dark') { + setTheme('light'); + } else { + setTheme('dark'); + } + }; + + return ( + + ); +} \ No newline at end of file diff --git a/frontend/src/components/ui/button.tsx b/frontend/src/components/ui/button.tsx new file mode 100644 index 0000000..a2df8dc --- /dev/null +++ b/frontend/src/components/ui/button.tsx @@ -0,0 +1,59 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + { + variants: { + variant: { + default: + "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", + destructive: + "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", + secondary: + "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", + ghost: + "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2 has-[>svg]:px-3", + sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", + lg: "h-10 rounded-md px-6 has-[>svg]:px-4", + icon: "size-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +function Button({ + className, + variant, + size, + asChild = false, + ...props +}: React.ComponentProps<"button"> & + VariantProps & { + asChild?: boolean + }) { + const Comp = asChild ? Slot : "button" + + return ( + + ) +} + +export { Button, buttonVariants } diff --git a/frontend/src/components/ui/card.tsx b/frontend/src/components/ui/card.tsx new file mode 100644 index 0000000..d05bbc6 --- /dev/null +++ b/frontend/src/components/ui/card.tsx @@ -0,0 +1,92 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Card({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardDescription({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardAction({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardAction, + CardDescription, + CardContent, +} diff --git a/frontend/src/components/ui/dialog.tsx b/frontend/src/components/ui/dialog.tsx new file mode 100644 index 0000000..7d7a9d3 --- /dev/null +++ b/frontend/src/components/ui/dialog.tsx @@ -0,0 +1,135 @@ +"use client" + +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { XIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Dialog({ + ...props +}: React.ComponentProps) { + return +} + +function DialogTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function DialogPortal({ + ...props +}: React.ComponentProps) { + return +} + +function DialogClose({ + ...props +}: React.ComponentProps) { + return +} + +function DialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogContent({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + {children} + + + Close + + + + ) +} + +function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +} diff --git a/frontend/src/components/ui/form.tsx b/frontend/src/components/ui/form.tsx new file mode 100644 index 0000000..524b986 --- /dev/null +++ b/frontend/src/components/ui/form.tsx @@ -0,0 +1,167 @@ +"use client" + +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { Slot } from "@radix-ui/react-slot" +import { + Controller, + FormProvider, + useFormContext, + useFormState, + type ControllerProps, + type FieldPath, + type FieldValues, +} from "react-hook-form" + +import { cn } from "@/lib/utils" +import { Label } from "@/components/ui/label" + +const Form = FormProvider + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +> = { + name: TName +} + +const FormFieldContext = React.createContext( + {} as FormFieldContextValue +) + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +>({ + ...props +}: ControllerProps) => { + return ( + + + + ) +} + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext) + const itemContext = React.useContext(FormItemContext) + const { getFieldState } = useFormContext() + const formState = useFormState({ name: fieldContext.name }) + const fieldState = getFieldState(fieldContext.name, formState) + + if (!fieldContext) { + throw new Error("useFormField should be used within ") + } + + const { id } = itemContext + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + } +} + +type FormItemContextValue = { + id: string +} + +const FormItemContext = React.createContext( + {} as FormItemContextValue +) + +function FormItem({ className, ...props }: React.ComponentProps<"div">) { + const id = React.useId() + + return ( + +
+ + ) +} + +function FormLabel({ + className, + ...props +}: React.ComponentProps) { + const { error, formItemId } = useFormField() + + return ( +