diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..8d35929 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,9 @@ +{ + "semi": true, + "trailingComma": "es5", + "singleQuote": true, + "printWidth": 100, + "tabWidth": 2, + "useTabs": false, + "plugins": ["prettier-plugin-tailwindcss"] +} diff --git a/README.md b/README.md index e215bc4..07bb6be 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,157 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). +# ColaFlow Web + +AI-Powered Project Management System - Frontend + +## Tech Stack + +- **Framework**: Next.js 15 (App Router) +- **Language**: TypeScript 5.x +- **UI Components**: shadcn/ui + Radix UI +- **Styling**: Tailwind CSS 4 +- **State Management**: + - TanStack Query v5 (server state) + - Zustand (client state) +- **Form Handling**: React Hook Form + Zod +- **Icons**: Lucide React + +## Project Structure + +``` +colaflow-web/ +├── app/ # Next.js App Router +│ ├── (auth)/ # Authentication routes +│ ├── (dashboard)/ # Main application routes +│ │ ├── dashboard/ # Dashboard page +│ │ ├── projects/ # Projects management +│ │ └── kanban/ # Kanban board +│ ├── layout.tsx # Root layout +│ └── page.tsx # Home page (redirects to dashboard) +├── components/ +│ ├── ui/ # shadcn/ui components +│ ├── features/ # Feature-specific components +│ │ ├── projects/ # Project components +│ │ └── kanban/ # Kanban components +│ └── layout/ # Layout components (Header, Sidebar) +├── lib/ +│ ├── api/ # API client and endpoints +│ ├── hooks/ # Custom React hooks +│ ├── providers/ # Context providers +│ └── utils.ts # Utility functions +├── stores/ # Zustand stores +├── types/ # TypeScript type definitions +└── public/ # Static assets +``` ## Getting Started -First, run the development server: +### Prerequisites + +- Node.js 20 LTS or higher +- npm or yarn + +### Installation + +1. Install dependencies: +```bash +npm install +``` + +2. Create `.env.local` file: +```env +NEXT_PUBLIC_API_URL=http://localhost:5000/api/v1 +``` + +### Development + +Start the development server: ```bash npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev ``` -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. +Open [http://localhost:3000](http://localhost:3000) in your browser. -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. +### Build -This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. +Build the production application: -## Learn More +```bash +npm run build +``` -To learn more about Next.js, take a look at the following resources: +### Linting & Formatting -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. +```bash +# Run ESLint +npm run lint -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! +# Format code with Prettier +npm run format +``` -## Deploy on Vercel +## Features Implemented (Sprint 1) -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. +### Core Pages +- Dashboard with project overview +- Projects list page with search and filtering +- Project detail page +- Kanban board (static, drag-and-drop in Sprint 2) -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. +### Components +- Responsive layout with sidebar navigation +- Project creation dialog with form validation +- Kanban board with column and task cards +- shadcn/ui integration (Button, Card, Dialog, Form, Input, Table) + +### State Management +- TanStack Query for server state +- Zustand for UI state (sidebar, theme, modals, notifications) +- Custom hooks for projects and kanban + +### API Integration +- Type-safe API client with error handling +- Projects API endpoints +- Kanban board API integration + +## TypeScript Types + +All API responses and data models are fully typed: + +- `types/project.ts` - Project, Epic, Story, Task types +- `types/kanban.ts` - Kanban board types +- `types/user.ts` - User and authentication types + +## API Endpoints + +The frontend expects the following API endpoints: + +- `GET /api/v1/projects` - List all projects +- `POST /api/v1/projects` - Create project +- `GET /api/v1/projects/{id}` - Get project details +- `PUT /api/v1/projects/{id}` - Update project +- `DELETE /api/v1/projects/{id}` - Delete project +- `GET /api/v1/projects/{id}/kanban` - Get kanban board + +## Environment Variables + +| Variable | Description | Default | +|----------|-------------|---------| +| `NEXT_PUBLIC_API_URL` | Backend API URL | `http://localhost:5000/api/v1` | + +## Next Steps (Sprint 2) + +- Implement drag-and-drop for Kanban board +- Add task creation and editing +- Implement authentication (login/register) +- Add user profile management +- Implement real-time updates with SignalR +- Add workflow customization +- Implement audit logs viewer + +## Architecture Reference + +For complete architecture details, see: `docs/M1-Architecture-Design.md` + +## License + +Proprietary - ColaFlow Project diff --git a/app/(dashboard)/dashboard/page.tsx b/app/(dashboard)/dashboard/page.tsx new file mode 100644 index 0000000..c523578 --- /dev/null +++ b/app/(dashboard)/dashboard/page.tsx @@ -0,0 +1,108 @@ +'use client'; + +import Link from 'next/link'; +import { FolderKanban, Plus } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { useProjects } from '@/lib/hooks/use-projects'; + +export default function DashboardPage() { + const { data: projects } = useProjects(); + + return ( +
+
+

Dashboard

+

+ Welcome to ColaFlow - Your AI-powered project management system +

+
+ +
+ + + Total Projects + + + +
{projects?.length || 0}
+

+ Active projects in your workspace +

+
+
+ + + + Active Projects + + + +
+ {projects?.filter((p) => p.status === 'Active').length || 0} +
+

+ Currently in progress +

+
+
+ + + + Quick Actions + + + + + + + + +
+ + + + Recent Projects + + Your most recently updated projects + + + + {projects && projects.length > 0 ? ( +
+ {projects.slice(0, 5).map((project) => ( + +
+
+

{project.name}

+

{project.key}

+
+ + {project.status} + +
+ + ))} +
+ ) : ( +

+ No projects yet. Create your first project to get started. +

+ )} +
+
+
+ ); +} diff --git a/app/(dashboard)/kanban/[projectId]/page.tsx b/app/(dashboard)/kanban/[projectId]/page.tsx new file mode 100644 index 0000000..8682dd9 --- /dev/null +++ b/app/(dashboard)/kanban/[projectId]/page.tsx @@ -0,0 +1,57 @@ +'use client'; + +import { use } from 'react'; +import Link from 'next/link'; +import { ArrowLeft, Loader2 } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { KanbanBoard } from '@/components/features/kanban/KanbanBoard'; +import { useKanbanBoard } from '@/lib/hooks/use-kanban'; + +interface KanbanPageProps { + params: Promise<{ projectId: string }>; +} + +export default function KanbanPage({ params }: KanbanPageProps) { + const { projectId } = use(params); + const { data: board, isLoading, error } = useKanbanBoard(projectId); + + if (isLoading) { + return ( +
+ +
+ ); + } + + if (error || !board) { + return ( +
+ + + +
+

+ Failed to load kanban board. Please try again later. +

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

Kanban Board

+
+ + +
+ ); +} diff --git a/app/(dashboard)/layout.tsx b/app/(dashboard)/layout.tsx new file mode 100644 index 0000000..84dc138 --- /dev/null +++ b/app/(dashboard)/layout.tsx @@ -0,0 +1,29 @@ +'use client'; + +import { Header } from '@/components/layout/Header'; +import { Sidebar } from '@/components/layout/Sidebar'; +import { useUIStore } from '@/stores/ui-store'; + +export default function DashboardLayout({ + children, +}: { + children: React.ReactNode; +}) { + const sidebarOpen = useUIStore((state) => state.sidebarOpen); + + return ( +
+
+
+ +
+
{children}
+
+
+
+ ); +} diff --git a/app/(dashboard)/projects/[id]/page.tsx b/app/(dashboard)/projects/[id]/page.tsx new file mode 100644 index 0000000..beb7ab9 --- /dev/null +++ b/app/(dashboard)/projects/[id]/page.tsx @@ -0,0 +1,91 @@ +'use client'; + +import { use } from 'react'; +import Link from 'next/link'; +import { ArrowLeft, Loader2, KanbanSquare } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { useProject } from '@/lib/hooks/use-projects'; + +interface ProjectDetailPageProps { + params: Promise<{ id: string }>; +} + +export default function ProjectDetailPage({ params }: ProjectDetailPageProps) { + const { id } = use(params); + const { data: project, isLoading, error } = useProject(id); + + if (isLoading) { + return ( +
+ +
+ ); + } + + if (error || !project) { + return ( +
+

+ Project not found or failed to load. +

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

{project.name}

+ + {project.status} + +
+

Key: {project.key}

+
+ + + +
+ + + + Project Details + Information about this project + + +
+

Description

+

{project.description || 'No description provided'}

+
+
+

Created

+

{new Date(project.createdAt).toLocaleDateString()}

+
+ {project.updatedAt && ( +
+

Last Updated

+

{new Date(project.updatedAt).toLocaleDateString()}

+
+ )} +
+
+
+ ); +} diff --git a/app/(dashboard)/projects/page.tsx b/app/(dashboard)/projects/page.tsx new file mode 100644 index 0000000..2c1d4cd --- /dev/null +++ b/app/(dashboard)/projects/page.tsx @@ -0,0 +1,95 @@ +'use client'; + +import { useState } from 'react'; +import Link from 'next/link'; +import { Plus, Loader2 } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { useProjects } from '@/lib/hooks/use-projects'; +import { CreateProjectDialog } from '@/components/features/projects/CreateProjectDialog'; + +export default function ProjectsPage() { + const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false); + const { data: projects, isLoading, error } = useProjects(); + + if (isLoading) { + return ( +
+ +
+ ); + } + + if (error) { + return ( +
+

+ Failed to load projects. Please try again later. +

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

Projects

+

+ Manage your projects and track progress +

+
+ +
+ +
+ {projects?.map((project) => ( + + + +
+
+ {project.name} + {project.key} +
+ + {project.status} + +
+
+ +

+ {project.description} +

+
+
+ + ))} + + {!projects || projects.length === 0 ? ( + + +

+ No projects yet. Create your first project to get started. +

+
+
+ ) : null} +
+ + +
+ ); +} diff --git a/app/globals.css b/app/globals.css index a2dc41e..dc98be7 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,26 +1,122 @@ @import "tailwindcss"; +@import "tw-animate-css"; -:root { - --background: #ffffff; - --foreground: #171717; -} +@custom-variant dark (&:is(.dark *)); @theme inline { --color-background: var(--background); --color-foreground: var(--foreground); --font-sans: var(--font-geist-sans); --font-mono: var(--font-geist-mono); + --color-sidebar-ring: var(--sidebar-ring); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar: var(--sidebar); + --color-chart-5: var(--chart-5); + --color-chart-4: var(--chart-4); + --color-chart-3: var(--chart-3); + --color-chart-2: var(--chart-2); + --color-chart-1: var(--chart-1); + --color-ring: var(--ring); + --color-input: var(--input); + --color-border: var(--border); + --color-destructive: var(--destructive); + --color-accent-foreground: var(--accent-foreground); + --color-accent: var(--accent); + --color-muted-foreground: var(--muted-foreground); + --color-muted: var(--muted); + --color-secondary-foreground: var(--secondary-foreground); + --color-secondary: var(--secondary); + --color-primary-foreground: var(--primary-foreground); + --color-primary: var(--primary); + --color-popover-foreground: var(--popover-foreground); + --color-popover: var(--popover); + --color-card-foreground: var(--card-foreground); + --color-card: var(--card); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); } -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; +:root { + --radius: 0.625rem; + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); +} + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; } } - -body { - background: var(--background); - color: var(--foreground); - font-family: Arial, Helvetica, sans-serif; -} diff --git a/app/layout.tsx b/app/layout.tsx index f7fa87e..fa7df7d 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,6 +1,7 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; +import { QueryProvider } from "@/lib/providers/query-provider"; const geistSans = Geist({ variable: "--font-geist-sans", @@ -13,8 +14,8 @@ const geistMono = Geist_Mono({ }); export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: "ColaFlow - AI-Powered Project Management", + description: "Modern project management system with AI and MCP protocol", }; export default function RootLayout({ @@ -27,7 +28,7 @@ export default function RootLayout({ - {children} + {children} ); diff --git a/app/page.tsx b/app/page.tsx index 295f8fd..f889cb6 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,65 +1,5 @@ -import Image from "next/image"; +import { redirect } from 'next/navigation'; export default function Home() { - return ( -
-
- Next.js logo -
-

- To get started, edit the page.tsx file. -

-

- Looking for a starting point or more instructions? Head over to{" "} - - Templates - {" "} - or the{" "} - - Learning - {" "} - center. -

-
-
- - Vercel logomark - Deploy Now - - - Documentation - -
-
-
- ); + redirect('/dashboard'); } diff --git a/components.json b/components.json new file mode 100644 index 0000000..b7b9791 --- /dev/null +++ b/components.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "registries": {} +} diff --git a/components/features/kanban/KanbanBoard.tsx b/components/features/kanban/KanbanBoard.tsx new file mode 100644 index 0000000..05f9bce --- /dev/null +++ b/components/features/kanban/KanbanBoard.tsx @@ -0,0 +1,29 @@ +'use client'; + +import { KanbanColumn } from './KanbanColumn'; +import type { KanbanBoard as KanbanBoardType } from '@/types/kanban'; + +interface KanbanBoardProps { + board: KanbanBoardType; +} + +export function KanbanBoard({ board }: KanbanBoardProps) { + return ( +
+
+

{board.projectName}

+

+ Total tasks: {board.columns.reduce((acc, col) => acc + col.tasks.length, 0)} +

+
+
+ {board.columns.map((column) => ( + + ))} +
+

+ Note: Drag-and-drop functionality will be implemented in Sprint 2 +

+
+ ); +} diff --git a/components/features/kanban/KanbanColumn.tsx b/components/features/kanban/KanbanColumn.tsx new file mode 100644 index 0000000..fd1b321 --- /dev/null +++ b/components/features/kanban/KanbanColumn.tsx @@ -0,0 +1,39 @@ +'use client'; + +import { TaskCard } from './TaskCard'; +import type { KanbanColumn as KanbanColumnType } from '@/types/kanban'; + +interface KanbanColumnProps { + column: KanbanColumnType; +} + +export function KanbanColumn({ column }: KanbanColumnProps) { + const statusColors = { + ToDo: 'border-gray-300', + InProgress: 'border-blue-300', + InReview: 'border-yellow-300', + Done: 'border-green-300', + Blocked: 'border-red-300', + }; + + return ( +
+
+

{column.title}

+ + {column.tasks.length} + +
+
+ {column.tasks.map((task) => ( + + ))} + {column.tasks.length === 0 && ( +
+

No tasks

+
+ )} +
+
+ ); +} diff --git a/components/features/kanban/TaskCard.tsx b/components/features/kanban/TaskCard.tsx new file mode 100644 index 0000000..b3c8577 --- /dev/null +++ b/components/features/kanban/TaskCard.tsx @@ -0,0 +1,62 @@ +'use client'; + +import { Card, CardContent, CardHeader } from '@/components/ui/card'; +import { Clock, User } from 'lucide-react'; +import type { TaskCard as TaskCardType } from '@/types/kanban'; + +interface TaskCardProps { + task: TaskCardType; + isDragging?: boolean; +} + +export function TaskCard({ task, isDragging = false }: TaskCardProps) { + const priorityColors = { + Low: 'bg-blue-100 text-blue-700', + Medium: 'bg-yellow-100 text-yellow-700', + High: 'bg-orange-100 text-orange-700', + Urgent: 'bg-red-100 text-red-700', + }; + + return ( + + +
+

{task.title}

+ + {task.priority} + +
+
+ + {task.description && ( +

+ {task.description} +

+ )} +
+ {task.estimatedHours && ( +
+ + {task.estimatedHours}h +
+ )} + {task.assigneeId && ( +
+ + Assigned +
+ )} +
+
+
+ ); +} diff --git a/components/features/projects/CreateProjectDialog.tsx b/components/features/projects/CreateProjectDialog.tsx new file mode 100644 index 0000000..d7256ee --- /dev/null +++ b/components/features/projects/CreateProjectDialog.tsx @@ -0,0 +1,162 @@ +'use client'; + +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import * as z from 'zod'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { Button } from '@/components/ui/button'; +import { useCreateProject } from '@/lib/hooks/use-projects'; +import type { CreateProjectDto } from '@/types/project'; + +const projectSchema = z.object({ + name: z.string().min(1, 'Project name is required').max(200, 'Project name cannot exceed 200 characters'), + description: z.string().max(2000, 'Description cannot exceed 2000 characters'), + key: z + .string() + .min(2, 'Project key must be at least 2 characters') + .max(10, 'Project key cannot exceed 10 characters') + .regex(/^[A-Z]+$/, 'Project key must contain only uppercase letters'), +}); + +interface CreateProjectDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; +} + +export function CreateProjectDialog({ + open, + onOpenChange, +}: CreateProjectDialogProps) { + const createProject = useCreateProject(); + + const form = useForm({ + resolver: zodResolver(projectSchema), + defaultValues: { + name: '', + description: '', + key: '', + }, + }); + + const onSubmit = async (data: CreateProjectDto) => { + try { + // TODO: Replace with actual user ID from auth context + const projectData = { + ...data, + ownerId: '00000000-0000-0000-0000-000000000001', + }; + await createProject.mutateAsync(projectData); + form.reset(); + onOpenChange(false); + } catch (error) { + console.error('Failed to create project:', error); + } + }; + + return ( + + + + Create New Project + + Add a new project to organize your work. Click create when you're done. + + + +
+ + ( + + Project Name + + + + + The name of your project. + + + + )} + /> + + ( + + Project Key + + { + field.onChange(e.target.value.toUpperCase()); + }} + /> + + + A unique identifier for the project (2-10 uppercase letters). + + + + )} + /> + + ( + + Description + + + + + Optional description for your project. + + + + )} + /> + + + + + + + +
+
+ ); +} diff --git a/components/layout/Header.tsx b/components/layout/Header.tsx new file mode 100644 index 0000000..17bf53f --- /dev/null +++ b/components/layout/Header.tsx @@ -0,0 +1,33 @@ +'use client'; + +import { Menu } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { useUIStore } from '@/stores/ui-store'; + +export function Header() { + const toggleSidebar = useUIStore((state) => state.toggleSidebar); + + return ( +
+
+ + +
+

ColaFlow

+
+ +
+ {/* Add user menu, notifications, etc. here */} +
+
+
+ ); +} diff --git a/components/layout/Sidebar.tsx b/components/layout/Sidebar.tsx new file mode 100644 index 0000000..c1e455e --- /dev/null +++ b/components/layout/Sidebar.tsx @@ -0,0 +1,59 @@ +'use client'; + +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; +import { LayoutDashboard, FolderKanban, Settings } from 'lucide-react'; +import { cn } from '@/lib/utils'; +import { useUIStore } from '@/stores/ui-store'; + +const navItems = [ + { + title: 'Dashboard', + href: '/dashboard', + icon: LayoutDashboard, + }, + { + title: 'Projects', + href: '/projects', + icon: FolderKanban, + }, + { + title: 'Settings', + href: '/settings', + icon: Settings, + }, +]; + +export function Sidebar() { + const pathname = usePathname(); + const sidebarOpen = useUIStore((state) => state.sidebarOpen); + + if (!sidebarOpen) return null; + + return ( + + ); +} diff --git a/components/ui/button.tsx b/components/ui/button.tsx new file mode 100644 index 0000000..21409a0 --- /dev/null +++ b/components/ui/button.tsx @@ -0,0 +1,60 @@ +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 hover:bg-primary/90", + destructive: + "bg-destructive text-white 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 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", + "icon-sm": "size-8", + "icon-lg": "size-10", + }, + }, + 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/components/ui/card.tsx b/components/ui/card.tsx new file mode 100644 index 0000000..681ad98 --- /dev/null +++ b/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/components/ui/dialog.tsx b/components/ui/dialog.tsx new file mode 100644 index 0000000..d9ccec9 --- /dev/null +++ b/components/ui/dialog.tsx @@ -0,0 +1,143 @@ +"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, + 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({ + 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/components/ui/dropdown-menu.tsx b/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000..bbe6fb0 --- /dev/null +++ b/components/ui/dropdown-menu.tsx @@ -0,0 +1,257 @@ +"use client" + +import * as React from "react" +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" +import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function DropdownMenu({ + ...props +}: React.ComponentProps) { + return +} + +function DropdownMenuPortal({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuContent({ + className, + sideOffset = 4, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +function DropdownMenuGroup({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuItem({ + className, + inset, + variant = "default", + ...props +}: React.ComponentProps & { + inset?: boolean + variant?: "default" | "destructive" +}) { + return ( + + ) +} + +function DropdownMenuCheckboxItem({ + className, + children, + checked, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function DropdownMenuRadioGroup({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuRadioItem({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function DropdownMenuLabel({ + className, + inset, + ...props +}: React.ComponentProps & { + inset?: boolean +}) { + return ( + + ) +} + +function DropdownMenuSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuShortcut({ + className, + ...props +}: React.ComponentProps<"span">) { + return ( + + ) +} + +function DropdownMenuSub({ + ...props +}: React.ComponentProps) { + return +} + +function DropdownMenuSubTrigger({ + className, + inset, + children, + ...props +}: React.ComponentProps & { + inset?: boolean +}) { + return ( + + {children} + + + ) +} + +function DropdownMenuSubContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + DropdownMenu, + DropdownMenuPortal, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuLabel, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuSub, + DropdownMenuSubTrigger, + DropdownMenuSubContent, +} diff --git a/components/ui/form.tsx b/components/ui/form.tsx new file mode 100644 index 0000000..524b986 --- /dev/null +++ b/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 ( +