From 223a1af6c9702777fc64dd218b288c3bc2b1ec86 Mon Sep 17 00:00:00 2001 From: yosheng Date: Sat, 26 Jul 2025 00:02:47 +0800 Subject: [PATCH] feat: initial commit --- .bolt/config.json | 3 + .bolt/ignore | 2 + .bolt/prompt | 9 + .eslintrc.json | 3 + .gitignore | 37 + app/detail/page.tsx | 104 + app/globals.css | 82 + app/layout.tsx | 25 + app/manage/page.tsx | 191 + app/page.tsx | 14 + app/todo/page.tsx | 147 + components.json | 20 + components/Layout.tsx | 54 + components/WorkItemCard.tsx | 53 + components/WorkItemForm.tsx | 72 + components/ui/accordion.tsx | 58 + components/ui/alert-dialog.tsx | 141 + components/ui/alert.tsx | 59 + components/ui/aspect-ratio.tsx | 7 + components/ui/avatar.tsx | 50 + components/ui/badge.tsx | 36 + components/ui/breadcrumb.tsx | 115 + components/ui/button.tsx | 56 + components/ui/calendar.tsx | 66 + components/ui/card.tsx | 86 + components/ui/carousel.tsx | 262 ++ components/ui/chart.tsx | 365 ++ components/ui/checkbox.tsx | 30 + components/ui/collapsible.tsx | 11 + components/ui/command.tsx | 155 + components/ui/context-menu.tsx | 200 + components/ui/dialog.tsx | 122 + components/ui/drawer.tsx | 118 + components/ui/dropdown-menu.tsx | 200 + components/ui/form.tsx | 179 + components/ui/hover-card.tsx | 29 + components/ui/input-otp.tsx | 71 + components/ui/input.tsx | 25 + components/ui/label.tsx | 26 + components/ui/menubar.tsx | 236 + components/ui/navigation-menu.tsx | 128 + components/ui/pagination.tsx | 117 + components/ui/popover.tsx | 31 + components/ui/progress.tsx | 28 + components/ui/radio-group.tsx | 44 + components/ui/resizable.tsx | 45 + components/ui/scroll-area.tsx | 48 + components/ui/select.tsx | 160 + components/ui/separator.tsx | 31 + components/ui/sheet.tsx | 140 + components/ui/skeleton.tsx | 15 + components/ui/slider.tsx | 28 + components/ui/sonner.tsx | 31 + components/ui/switch.tsx | 29 + components/ui/table.tsx | 117 + components/ui/tabs.tsx | 55 + components/ui/textarea.tsx | 24 + components/ui/toast.tsx | 129 + components/ui/toaster.tsx | 35 + components/ui/toggle-group.tsx | 61 + components/ui/toggle.tsx | 45 + components/ui/tooltip.tsx | 30 + hooks/use-toast.ts | 191 + lib/api-client.ts | 78 + lib/types.ts | 16 + lib/utils.ts | 6 + next.config.js | 10 + package-lock.json | 7168 +++++++++++++++++++++++++++++ package.json | 74 + postcss.config.js | 6 + tailwind.config.ts | 90 + tsconfig.json | 27 + 72 files changed, 12556 insertions(+) create mode 100644 .bolt/config.json create mode 100644 .bolt/ignore create mode 100644 .bolt/prompt create mode 100644 .eslintrc.json create mode 100644 .gitignore create mode 100644 app/detail/page.tsx create mode 100644 app/globals.css create mode 100644 app/layout.tsx create mode 100644 app/manage/page.tsx create mode 100644 app/page.tsx create mode 100644 app/todo/page.tsx create mode 100644 components.json create mode 100644 components/Layout.tsx create mode 100644 components/WorkItemCard.tsx create mode 100644 components/WorkItemForm.tsx create mode 100644 components/ui/accordion.tsx create mode 100644 components/ui/alert-dialog.tsx create mode 100644 components/ui/alert.tsx create mode 100644 components/ui/aspect-ratio.tsx create mode 100644 components/ui/avatar.tsx create mode 100644 components/ui/badge.tsx create mode 100644 components/ui/breadcrumb.tsx create mode 100644 components/ui/button.tsx create mode 100644 components/ui/calendar.tsx create mode 100644 components/ui/card.tsx create mode 100644 components/ui/carousel.tsx create mode 100644 components/ui/chart.tsx create mode 100644 components/ui/checkbox.tsx create mode 100644 components/ui/collapsible.tsx create mode 100644 components/ui/command.tsx create mode 100644 components/ui/context-menu.tsx create mode 100644 components/ui/dialog.tsx create mode 100644 components/ui/drawer.tsx create mode 100644 components/ui/dropdown-menu.tsx create mode 100644 components/ui/form.tsx create mode 100644 components/ui/hover-card.tsx create mode 100644 components/ui/input-otp.tsx create mode 100644 components/ui/input.tsx create mode 100644 components/ui/label.tsx create mode 100644 components/ui/menubar.tsx create mode 100644 components/ui/navigation-menu.tsx create mode 100644 components/ui/pagination.tsx create mode 100644 components/ui/popover.tsx create mode 100644 components/ui/progress.tsx create mode 100644 components/ui/radio-group.tsx create mode 100644 components/ui/resizable.tsx create mode 100644 components/ui/scroll-area.tsx create mode 100644 components/ui/select.tsx create mode 100644 components/ui/separator.tsx create mode 100644 components/ui/sheet.tsx create mode 100644 components/ui/skeleton.tsx create mode 100644 components/ui/slider.tsx create mode 100644 components/ui/sonner.tsx create mode 100644 components/ui/switch.tsx create mode 100644 components/ui/table.tsx create mode 100644 components/ui/tabs.tsx create mode 100644 components/ui/textarea.tsx create mode 100644 components/ui/toast.tsx create mode 100644 components/ui/toaster.tsx create mode 100644 components/ui/toggle-group.tsx create mode 100644 components/ui/toggle.tsx create mode 100644 components/ui/tooltip.tsx create mode 100644 hooks/use-toast.ts create mode 100644 lib/api-client.ts create mode 100644 lib/types.ts create mode 100644 lib/utils.ts create mode 100644 next.config.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 postcss.config.js create mode 100644 tailwind.config.ts create mode 100644 tsconfig.json diff --git a/.bolt/config.json b/.bolt/config.json new file mode 100644 index 0000000..f236591 --- /dev/null +++ b/.bolt/config.json @@ -0,0 +1,3 @@ +{ + "template": "nextjs-shadcn" +} diff --git a/.bolt/ignore b/.bolt/ignore new file mode 100644 index 0000000..bbe3a15 --- /dev/null +++ b/.bolt/ignore @@ -0,0 +1,2 @@ +components/ui/* +hooks/use-toast.ts diff --git a/.bolt/prompt b/.bolt/prompt new file mode 100644 index 0000000..88d020b --- /dev/null +++ b/.bolt/prompt @@ -0,0 +1,9 @@ +For all designs I ask you to make, have them be beautiful, not cookie cutter. Make webpages that are fully featured and worthy for production. + +When using client-side hooks (useState and useEffect) in a component that's being treated as a Server Component by Next.js, always add the "use client" directive at the top of the file. + +Do not write code that will trigger this error: "Warning: Extra attributes from the server: %s%s""class,style" + +By default, this template supports JSX syntax with Tailwind CSS classes, the shadcn/ui library, React hooks, and Lucide React for icons. Do not install other packages for UI themes, icons, etc unless absolutely necessary or I request them. + +Use icons from lucide-react for logos. diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..bffb357 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..00b581b --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. +.idea + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/app/detail/page.tsx b/app/detail/page.tsx new file mode 100644 index 0000000..804c492 --- /dev/null +++ b/app/detail/page.tsx @@ -0,0 +1,104 @@ + +'use client'; + +import { useState, useEffect } from 'react'; +import { useParams } from 'next/navigation'; +import { WorkItem, WorkItemStatus } from '@/lib/types'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faInfoCircle } from '@fortawesome/free-solid-svg-icons'; +import { workItemApi } from '@/lib/api-client'; + +export default function DetailPage() { + const params = useParams(); + const [workItem, setWorkItem] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + // Ensure we have a valid ID string and convert to number + const idString = Array.isArray(params.id) ? params.id[0] : params.id; + const id = idString ? parseInt(idString, 10) : NaN; + + if (!isNaN(id)) { + // Fetch work item asynchronously + workItemApi.getWorkItem(id).then(item => { + setWorkItem(item || null); + setLoading(false); + }).catch(error => { + console.error('Error fetching work item:', error); + setWorkItem(null); + setLoading(false); + }); + } else { + setLoading(false); + } + }, [params.id]); + + if (loading) { + return ( +
+
+
+

Loading...

+
+
+
+ ); + } + + if (!workItem) { + return ( +
+
+
+

Work item not found.

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

+ + Work Item Detail +

+
+ +
+
+ +

{workItem.id}

+
+ +
+ +

{workItem.title}

+
+ +
+ +

{workItem.description}

+
+ +
+ + + {workItem.status} + +
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/app/globals.css b/app/globals.css new file mode 100644 index 0000000..20b1c1d --- /dev/null +++ b/app/globals.css @@ -0,0 +1,82 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --foreground-rgb: 0, 0, 0; + --background-start-rgb: 214, 219, 220; + --background-end-rgb: 255, 255, 255; +} + +@media (prefers-color-scheme: dark) { + :root { + --foreground-rgb: 255, 255, 255; + --background-start-rgb: 0, 0, 0; + --background-end-rgb: 0, 0, 0; + } +} + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 0 0% 3.9%; + --card: 0 0% 100%; + --card-foreground: 0 0% 3.9%; + --popover: 0 0% 100%; + --popover-foreground: 0 0% 3.9%; + --primary: 0 0% 9%; + --primary-foreground: 0 0% 98%; + --secondary: 0 0% 96.1%; + --secondary-foreground: 0 0% 9%; + --muted: 0 0% 96.1%; + --muted-foreground: 0 0% 45.1%; + --accent: 0 0% 96.1%; + --accent-foreground: 0 0% 9%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 0 0% 98%; + --border: 0 0% 89.8%; + --input: 0 0% 89.8%; + --ring: 0 0% 3.9%; + --chart-1: 12 76% 61%; + --chart-2: 173 58% 39%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; + --radius: 0.5rem; + } + .dark { + --background: 0 0% 3.9%; + --foreground: 0 0% 98%; + --card: 0 0% 3.9%; + --card-foreground: 0 0% 98%; + --popover: 0 0% 3.9%; + --popover-foreground: 0 0% 98%; + --primary: 0 0% 98%; + --primary-foreground: 0 0% 9%; + --secondary: 0 0% 14.9%; + --secondary-foreground: 0 0% 98%; + --muted: 0 0% 14.9%; + --muted-foreground: 0 0% 63.9%; + --accent: 0 0% 14.9%; + --accent-foreground: 0 0% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + --border: 0 0% 14.9%; + --input: 0 0% 14.9%; + --ring: 0 0% 83.1%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 0000000..c18105b --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,25 @@ +import './globals.css'; +import type { Metadata } from 'next'; +import { Inter } from 'next/font/google'; +import Layout from '@/components/Layout'; + +const inter = Inter({ subsets: ['latin'] }); + +export const metadata: Metadata = { + title: 'Work Item Manager', + description: 'Manage your work items efficiently', +}; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + {children} + + + ); +} \ No newline at end of file diff --git a/app/manage/page.tsx b/app/manage/page.tsx new file mode 100644 index 0000000..50430a9 --- /dev/null +++ b/app/manage/page.tsx @@ -0,0 +1,191 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { WorkItem, WorkItemFormData, WorkItemStatus } from '@/lib/types'; +import { workItemApi } from '@/lib/api-client'; +import WorkItemForm from '@/components/WorkItemForm'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faCog, faEdit, faTrash } from '@fortawesome/free-solid-svg-icons'; + +export default function ManagePage() { + const [workItems, setWorkItems] = useState([]); + const [editingItem, setEditingItem] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + loadWorkItems(); + }, []); + + const loadWorkItems = async () => { + try { + setLoading(true); + const items = await workItemApi.getWorkItems(); + setWorkItems(items); + setError(null); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to load work items'); + } finally { + setLoading(false); + } + }; + + const handleCreate = async (data: WorkItemFormData) => { + try { + const newItem = await workItemApi.createWorkItem(data); + setWorkItems([...workItems, newItem]); + } catch (err) { + alert('Failed to create work item. Please try again.'); + console.error('Error creating item:', err); + } + }; + + const handleEdit = (item: WorkItem) => { + setEditingItem(item); + }; + + const handleUpdate = async (data: WorkItemFormData) => { + if (editingItem) { + try { + const updatedItem = { ...editingItem, title: data.title, description: data.description }; + await workItemApi.updateWorkItem(editingItem.id, updatedItem); + await loadWorkItems(); + setEditingItem(null); + } catch (err) { + alert('Failed to update work item. Please try again.'); + console.error('Error updating item:', err); + } + } + }; + + const handleDelete = async (id: number) => { + if (confirm('Are you sure you want to delete this item?')) { + try { + await workItemApi.deleteWorkItem(id); + await loadWorkItems(); + } catch (err) { + alert('Failed to delete work item. Please try again.'); + console.error('Error deleting item:', err); + } + } + }; + + const handleCancelEdit = () => { + setEditingItem(null); + }; + + if (loading) { + return ( +
+
+
+
+

+ + Admin Panel +

+
+
+ Loading work items... +
+
+
+
+ ); + } + + if (error) { + return ( +
+
+
+
+

+ + Admin Panel +

+
+
+ Error: {error} + +
+
+
+
+ ); + } + + return ( +
+
+
+
+

+ + Admin Panel +

+
+ +
+
+

+ {editingItem ? 'Edit Work Item' : 'Create New Work Item'} +

+ + {editingItem && ( + + )} +
+ +
+

Existing Work Items

+
+ {workItems.map((item) => ( +
+
+

{item.title}

+

{item.description}

+
+
+ + +
+
+ ))} +
+
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/app/page.tsx b/app/page.tsx new file mode 100644 index 0000000..9eeddc3 --- /dev/null +++ b/app/page.tsx @@ -0,0 +1,14 @@ +'use client'; + +import { useEffect } from 'react'; +import { useRouter } from 'next/navigation'; + +export default function Home() { + const router = useRouter(); + + useEffect(() => { + router.push('/todo'); + }, [router]); + + return null; +} \ No newline at end of file diff --git a/app/todo/page.tsx b/app/todo/page.tsx new file mode 100644 index 0000000..ec8398b --- /dev/null +++ b/app/todo/page.tsx @@ -0,0 +1,147 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { WorkItem, WorkItemStatus } from '@/lib/types'; +import { workItemApi } from '@/lib/api-client'; +import WorkItemCard from '@/components/WorkItemCard'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faCheck } from '@fortawesome/free-solid-svg-icons'; + +export default function TodoPage() { + const [workItems, setWorkItems] = useState([]); + const [selectedItems, setSelectedItems] = useState>(new Set()); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + loadWorkItems(); + }, []); + + const loadWorkItems = async () => { + try { + setLoading(true); + const items = await workItemApi.getWorkItems(); + setWorkItems(items); + setError(null); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to load work items'); + } finally { + setLoading(false); + } + }; + + const handleToggle = (id: number) => { + const newSelected = new Set(selectedItems); + if (newSelected.has(id)) { + newSelected.delete(id); + } else { + newSelected.add(id); + } + setSelectedItems(newSelected); + }; + + const handleConfirm = async () => { + if (selectedItems.size > 0) { + try { + // Update each selected item + for (const id of selectedItems) { + const item = workItems.find(item => item.id === id); + if (item) { + const updatedItem = { ...item, status: WorkItemStatus.Completed }; + await workItemApi.updateWorkItem(id, updatedItem); + } + } + + // Reload the list + await loadWorkItems(); + setSelectedItems(new Set()); + alert(`${selectedItems.size} item(s) marked as completed!`); + } catch (err) { + alert('Failed to update items. Please try again.'); + console.error('Error updating items:', err); + } + } + }; + + if (loading) { + return ( +
+
+
+
+

Work Item List

+
+
+ Loading work items... +
+
+
+
+ ); + } + + if (error) { + return ( +
+
+
+
+

Work Item List

+
+
+ Error: {error} + +
+
+
+
+ ); + } + + return ( +
+
+
+
+

Work Item List

+
+ +
+
+
+
Title
+
Status
+
+ +
+ {workItems.map((item) => ( + + ))} +
+
+ + +
+
+
+
+ ); +} \ No newline at end of file diff --git a/components.json b/components.json new file mode 100644 index 0000000..c597462 --- /dev/null +++ b/components.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + } +} diff --git a/components/Layout.tsx b/components/Layout.tsx new file mode 100644 index 0000000..3709384 --- /dev/null +++ b/components/Layout.tsx @@ -0,0 +1,54 @@ +'use client'; + +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faList, faEye, faCog } from '@fortawesome/free-solid-svg-icons'; + +interface LayoutProps { + children: React.ReactNode; +} + +export default function Layout({ children }: LayoutProps) { + const pathname = usePathname(); + + const navigation = [ + { name: 'Work Items', href: '/todo', icon: faList }, + { name: 'Admin Panel', href: '/manage', icon: faCog }, + ]; + + return ( +
+ +
+ {children} +
+
+ ); +} \ No newline at end of file diff --git a/components/WorkItemCard.tsx b/components/WorkItemCard.tsx new file mode 100644 index 0000000..4793286 --- /dev/null +++ b/components/WorkItemCard.tsx @@ -0,0 +1,53 @@ +'use client'; + +import { WorkItem, WorkItemStatus } from '@/lib/types'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faCheck } from '@fortawesome/free-solid-svg-icons'; + +interface WorkItemCardProps { + item: WorkItem; + showCheckbox?: boolean; + onToggle?: (id: number) => void; + selected?: boolean; +} + +export default function WorkItemCard({ + item, + showCheckbox = false, + onToggle, + selected = false +}: WorkItemCardProps) { + return ( +
+
+
+ {showCheckbox && ( + + )} +
+

{item.title}

+
+
+ + {item.status === WorkItemStatus.Pending ? 'Pending' : + item.status === WorkItemStatus.InProgress ? 'In Progress' : 'Completed'} + +
+
+ ); +} \ No newline at end of file diff --git a/components/WorkItemForm.tsx b/components/WorkItemForm.tsx new file mode 100644 index 0000000..c5c1d72 --- /dev/null +++ b/components/WorkItemForm.tsx @@ -0,0 +1,72 @@ +'use client'; + +import { useState } from 'react'; +import { WorkItemFormData } from '@/lib/types'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faSave } from '@fortawesome/free-solid-svg-icons'; + +interface WorkItemFormProps { + initialData?: WorkItemFormData; + onSubmit: (data: WorkItemFormData) => void; + submitText?: string; +} + +export default function WorkItemForm({ + initialData, + onSubmit, + submitText = 'Save' +}: WorkItemFormProps) { + const [formData, setFormData] = useState( + initialData || { title: '', description: '' } + ); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (formData.title.trim() && formData.description.trim()) { + onSubmit(formData); + if (!initialData) { + setFormData({ title: '', description: '' }); + } + } + }; + + return ( +
+
+ + setFormData({ ...formData, title: e.target.value })} + className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" + required + /> +
+ +
+ +