diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index dc98be7..0152397 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -118,5 +118,52 @@ } body { @apply bg-background text-foreground; + /* Improve mobile scrolling */ + -webkit-overflow-scrolling: touch; + scroll-behavior: smooth; + } + + /* Mobile-specific improvements */ + @media (max-width: 640px) { + /* Ensure minimum touch target size */ + button, [role="button"], input[type="button"], input[type="submit"], input[type="reset"] { + min-height: 44px; + min-width: 44px; + } + + /* Improve text readability on mobile */ + body { + font-size: 16px; /* Prevent zoom on iOS */ + line-height: 1.5; + } + + /* Better spacing for mobile */ + .space-y-6 > * + * { + margin-top: 1.5rem; + } + + /* Improve card interactions on mobile */ + .card { + transition: transform 0.2s ease, box-shadow 0.2s ease; + } + + .card:active { + transform: scale(0.98); + } + } + + /* Prevent horizontal scroll on mobile */ + html, body { + overflow-x: hidden; + } + + /* Improve focus states for accessibility */ + button:focus-visible, + [role="button"]:focus-visible, + input:focus-visible, + select:focus-visible, + textarea:focus-visible { + outline: 2px solid var(--ring); + outline-offset: 2px; } } diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index 625eb5e..136948e 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -9,10 +9,11 @@ * Copyright (c) 2024 Continuist */ -import type { Metadata } from "next"; +import type { Metadata, Viewport } from "next"; import { Inter } from "next/font/google"; import "./globals.css"; import Link from "next/link"; +import { MobileNav } from "@/components/mobile-nav"; const inter = Inter({ subsets: ["latin"] }); @@ -21,6 +22,13 @@ export const metadata: Metadata = { description: "Admin interface for Sharenet", }; +export const viewport: Viewport = { + width: 'device-width', + initialScale: 1, + maximumScale: 1, + userScalable: false, +}; + export default function RootLayout({ children, }: Readonly<{ @@ -58,10 +66,11 @@ export default function RootLayout({ + -
+
{children}
diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 62dacba..c48bdd4 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -16,26 +16,26 @@ export default function Home() { return (

Dashboard

-
- - +
+ + - Users + Users Manage system users -

View, create, edit, and delete users

+

View, create, edit, and delete users

- - + + - Products + Products Manage product catalog -

View, create, edit, and delete products

+

View, create, edit, and delete products

diff --git a/frontend/src/app/products/page.tsx b/frontend/src/app/products/page.tsx index 3a658b7..bb0fe34 100644 --- a/frontend/src/app/products/page.tsx +++ b/frontend/src/app/products/page.tsx @@ -14,14 +14,7 @@ import { useState, useEffect } from 'react'; import { productApi, Product } from '@/lib/api'; import { Button } from '@/components/ui/button'; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from '@/components/ui/table'; +import { ResponsiveTable } from '@/components/ui/responsive-table'; import { Dialog, DialogContent, @@ -126,21 +119,63 @@ export default function ProductsPage() { } }; + const columns = [ + { + key: 'name', + header: 'Name', + mobilePriority: true, + }, + { + key: 'description', + header: 'Description', + mobilePriority: true, + render: (value: unknown) => String(value || 'No description'), + }, + { + key: 'actions', + header: 'Actions', + mobilePriority: true, + render: (_: unknown, product: Record) => ( +
+ + +
+ ), + }, + ]; + return ( -
-
+
+

Products

- - + {editingProduct ? 'Edit Product' : 'Add Product'} @@ -165,41 +200,25 @@ export default function ProductsPage() { onChange={(e) => handleInputChange('description', e.target.value)} />
- +
+ + +
- - - - ID - Name - Description - Actions - - - - {products.map((product) => ( - - {product.id} - {product.name} - {product.description} - -
- - -
-
-
- ))} -
-
+ []} />
); } \ No newline at end of file diff --git a/frontend/src/app/users/page.tsx b/frontend/src/app/users/page.tsx index 1f11785..215b58c 100644 --- a/frontend/src/app/users/page.tsx +++ b/frontend/src/app/users/page.tsx @@ -14,14 +14,7 @@ import { useState, useEffect } from 'react'; import { userApi, User } from '@/lib/api'; import { Button } from '@/components/ui/button'; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from '@/components/ui/table'; +import { ResponsiveTable } from '@/components/ui/responsive-table'; import { Dialog, DialogContent, @@ -132,21 +125,74 @@ export default function UsersPage() { } }; + const columns = [ + { + key: 'username', + header: 'Username', + mobilePriority: true, + }, + { + key: 'email', + header: 'Email', + mobilePriority: true, + }, + { + key: 'created_at', + header: 'Created At', + mobilePriority: false, + render: (value: unknown) => new Date(value as string).toLocaleString(), + }, + { + key: 'updated_at', + header: 'Updated At', + mobilePriority: false, + render: (value: unknown) => new Date(value as string).toLocaleString(), + }, + { + key: 'actions', + header: 'Actions', + mobilePriority: true, + render: (_: unknown, user: Record) => ( +
+ + +
+ ), + }, + ]; + return ( -
-
+
+

Users

- - + {editingUser ? 'Edit User' : 'Add User'} @@ -176,45 +222,25 @@ export default function UsersPage() {

{errors.email}

)}
- +
+ + +
- - - - ID - Username - Email - Created At - Updated At - Actions - - - - {users.map((user) => ( - - {user.id} - {user.username} - {user.email} - {new Date(user.created_at).toLocaleString()} - {new Date(user.updated_at).toLocaleString()} - -
- - -
-
-
- ))} -
-
+ []} />
); } \ No newline at end of file diff --git a/frontend/src/components/mobile-nav.tsx b/frontend/src/components/mobile-nav.tsx new file mode 100644 index 0000000..4e506d8 --- /dev/null +++ b/frontend/src/components/mobile-nav.tsx @@ -0,0 +1,72 @@ +'use client'; + +import { useState } from 'react'; +import Link from 'next/link'; +import { Button } from '@/components/ui/button'; + +export function MobileNav() { + const [isOpen, setIsOpen] = useState(false); + + return ( + <> +
+ +
+ +
+
+ setIsOpen(false)} + > + Dashboard + + setIsOpen(false)} + > + Users + + setIsOpen(false)} + > + Products + +
+
+ + ); +} \ No newline at end of file diff --git a/frontend/src/components/ui/button.tsx b/frontend/src/components/ui/button.tsx index a2df8dc..d3bfe3d 100644 --- a/frontend/src/components/ui/button.tsx +++ b/frontend/src/components/ui/button.tsx @@ -5,7 +5,7 @@ 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", + "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 touch-manipulation", { variants: { variant: { @@ -22,10 +22,10 @@ const buttonVariants = cva( 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", + default: "h-10 px-4 py-2 has-[>svg]:px-3 sm:h-9 sm:px-4 sm:py-2", + sm: "h-9 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5 sm:h-8 sm:px-3", + lg: "h-12 rounded-md px-6 has-[>svg]:px-4 sm:h-10 sm:px-6", + icon: "size-12 sm:size-9", }, }, defaultVariants: { diff --git a/frontend/src/components/ui/card.tsx b/frontend/src/components/ui/card.tsx index d05bbc6..c8567de 100644 --- a/frontend/src/components/ui/card.tsx +++ b/frontend/src/components/ui/card.tsx @@ -2,91 +2,82 @@ import * as React from "react" import { cn } from "@/lib/utils" -function Card({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) -} +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +Card.displayName = "Card" -function CardHeader({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) -} +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardHeader.displayName = "CardHeader" -function CardTitle({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) -} +const CardTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardTitle.displayName = "CardTitle" -function CardDescription({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) -} +const CardDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardDescription.displayName = "CardDescription" -function CardAction({ className, ...props }: React.ComponentProps<"div">) { - return ( -

- ) -} +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardContent.displayName = "CardContent" -function CardContent({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) -} +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardFooter.displayName = "CardFooter" -function CardFooter({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) -} - -export { - Card, - CardHeader, - CardFooter, - CardTitle, - CardAction, - CardDescription, - CardContent, -} +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/frontend/src/components/ui/dialog.tsx b/frontend/src/components/ui/dialog.tsx index d9ccec9..50214f1 100644 --- a/frontend/src/components/ui/dialog.tsx +++ b/frontend/src/components/ui/dialog.tsx @@ -2,142 +2,123 @@ import * as React from "react" import * as DialogPrimitive from "@radix-ui/react-dialog" -import { XIcon } from "lucide-react" +import { X } from "lucide-react" import { cn } from "@/lib/utils" -function Dialog({ - ...props -}: React.ComponentProps) { - return -} +const Dialog = DialogPrimitive.Root -function DialogTrigger({ - ...props -}: React.ComponentProps) { - return -} +const DialogTrigger = DialogPrimitive.Trigger -function DialogPortal({ - ...props -}: React.ComponentProps) { - return -} +const DialogPortal = DialogPrimitive.Portal -function DialogClose({ - ...props -}: React.ComponentProps) { - return -} +const DialogClose = DialogPrimitive.Close -function DialogOverlay({ - className, - ...props -}: React.ComponentProps) { - return ( - , + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + - ) -} + > + {children} + + + Close + + + +)) +DialogContent.displayName = DialogPrimitive.Content.displayName -function DialogContent({ - className, - children, - showCloseButton = true, - ...props -}: React.ComponentProps & { - showCloseButton?: boolean -}) { - return ( - - - - {children} - {showCloseButton && ( - - - Close - - )} - - - ) -} - -function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) -} - -function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) -} - -function DialogTitle({ +const DialogHeader = ({ className, ...props -}: React.ComponentProps) { - return ( - - ) -} +}: React.HTMLAttributes) => ( +
+) +DialogHeader.displayName = "DialogHeader" -function DialogDescription({ +const DialogFooter = ({ className, ...props -}: React.ComponentProps) { - return ( - - ) -} +}: React.HTMLAttributes) => ( +
+) +DialogFooter.displayName = "DialogFooter" + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogTitle.displayName = DialogPrimitive.Title.displayName + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogDescription.displayName = DialogPrimitive.Description.displayName export { Dialog, - DialogClose, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogOverlay, DialogPortal, - DialogTitle, + DialogOverlay, + DialogClose, DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, } diff --git a/frontend/src/components/ui/input.tsx b/frontend/src/components/ui/input.tsx index 03295ca..632409f 100644 --- a/frontend/src/components/ui/input.tsx +++ b/frontend/src/components/ui/input.tsx @@ -2,20 +2,24 @@ import * as React from "react" import { cn } from "@/lib/utils" -function Input({ className, type, ...props }: React.ComponentProps<"input">) { - return ( - - ) -} +const Input = React.forwardRef>( + ({ className, type, ...props }, ref) => { + return ( + + ) + } +) +Input.displayName = "Input" export { Input } diff --git a/frontend/src/components/ui/responsive-table.tsx b/frontend/src/components/ui/responsive-table.tsx new file mode 100644 index 0000000..368da54 --- /dev/null +++ b/frontend/src/components/ui/responsive-table.tsx @@ -0,0 +1,91 @@ +'use client'; + +import { ReactNode } from 'react'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; + +interface Column { + key: string; + header: string; + render?: (value: unknown, row: Record) => ReactNode; + mobilePriority?: boolean; // Whether to show this column on mobile +} + +interface ResponsiveTableProps { + columns: Column[]; + data: Record[]; + className?: string; +} + +export function ResponsiveTable({ columns, data, className }: ResponsiveTableProps) { + const mobileColumns = columns.filter(col => col.mobilePriority !== false); + + return ( +
+ {/* Desktop Table */} +
+ + + + {columns.map((column) => ( + {column.header} + ))} + + + + {data.map((row, index) => ( + + {columns.map((column) => ( + + {column.render + ? column.render(row[column.key], row) + : String(row[column.key] || '') + } + + ))} + + ))} + +
+
+ + {/* Mobile Cards */} +
+ {data.map((row, index) => ( + + + + {mobileColumns[0]?.render + ? mobileColumns[0].render(row[mobileColumns[0]?.key || ''], row) + : String(row[mobileColumns[0]?.key || ''] || '') + } + + + + {mobileColumns.slice(1).map((column) => ( +
+ + {column.header}: + + + {column.render + ? column.render(row[column.key], row) + : String(row[column.key] || '') + } + +
+ ))} +
+
+ ))} +
+
+ ); +} \ No newline at end of file