commit d90aabec4b629cdff65ce0fa2dd04fae1d772aba Author: Bamboo Date: Tue May 12 20:17:52 2026 +0500 Application setup diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..91f3880 --- /dev/null +++ b/.gitignore @@ -0,0 +1,31 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +server/dist +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +.env* +!.env.example +.aider* + +src/lib/taylordb.types.ts \ No newline at end of file diff --git a/.ignore b/.ignore new file mode 100644 index 0000000..ed18058 --- /dev/null +++ b/.ignore @@ -0,0 +1,5 @@ +.env +.env.* +QUERY_BUILDER.md +.opencode +.ignore \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..9c47779 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,733 @@ +# AI Agent Instructions for TaylorDB Full-Stack Template + +This template is designed for AI agents to build **modern, type-safe, full-stack applications** using TaylorDB as the data source. This template provides a complete monorepo setup with a React frontend (Vite + shadcn/ui) and a Node.js backend (tRPC + TaylorDB query builder). + +--- + +## 🎯 Your Mission + +Build production-ready, modern web applications (primarily dashboards and CRUD interfaces) that: + +- Are **fully type-safe** from database to UI +- Use TaylorDB as the single source of truth for data +- Have **stunning, modern UI/UX** that wows users +- Follow best practices in architecture and code organization + +--- + +## 📚 Docs for Agents + +Before you start implementing, skim these docs in the `docs/` folder: + +- **TaylorDB Query Builder** + - `docs/TAYLORDB_QUERY_REFERENCE.md` – index for all query examples + - `docs/TAYLORDB_BASIC_QUERIES.md` – basic reads, filtering, dates + - `docs/TAYLORDB_WRITE_OPERATIONS.md` – inserts, updates, deletes + - `docs/TAYLORDB_ADVANCED_PATTERNS.md` – aggregations, pagination, conditional queries + - `docs/TAYLORDB_FIELD_TYPES.md` – field type mapping & enums + - `docs/TAYLORDB_ATTACHMENTS.md` – working with attachment fields + - `docs/TAYLORDB_PITFALLS_BEST_PRACTICES.md` – common mistakes & best practices + +- **shadcn/ui Components & Dashboard Patterns** + - `docs/SHADCN_COMPONENTS_GUIDE.md` – index for all shadcn/ui docs + - `docs/SHADCN_INSTALLATION.md` – how to install shadcn/ui components + - `docs/SHADCN_DASHBOARD_PATTERNS.md` – ready-made dashboard/layout patterns + - `docs/SHADCN_DESIGN_AND_LAYOUT.md` – design tokens, layout, responsive & performance tips + +Use `AGENTS.md` for **workflow and rules** and the `docs/` files for **detailed code examples**. + +--- + +## 📋 Development Workflow + +### **Phase 1: Understand Requirements & Design** + +#### Step 1: Gather Requirements + +Ask the user clarifying questions about: + +- What data they want to work with (understand the domain) +- Key features and user workflows +- Target users and use cases +- Any specific UI/UX preferences + +#### Step 2: Understand the Database Schema + +**CRITICAL**: Always start by reading these files to understand the data model: + +- `apps/server/taylordb/types.ts` - TypeScript types generated from TaylorDB schema +- `apps/server/taylordb/query-builder.ts` - Query builder patterns (review for examples) + +> ⚠️ **IMPORTANT**: If these files don't exist, STOP and ask the user to generate them first. Never proceed with mock data. + +#### Step 3: Design the Color Scheme & Visual Identity + +Based on the requirements, decide on: + +- **Primary color scheme** (use HSL values for flexibility) +- **Design aesthetic** (modern glassmorphism, gradients, minimalism, etc.) +- **Typography** (Google Fonts like Inter, Outfit, or Manrope) +- **Animation style** (subtle micro-interactions vs. bold animations) + +Document your design decisions briefly before implementing. + +--- + +### **Phase 2: Build the Foundation** + +#### Step 1: Set Up Server-Side Data Layer + +Use querybuilder which is in **File: `apps/server/taylordb/query-builder.ts`** + +You can access the query builder from + +```typescript +publicProcedure.input({}).query(({ input, ctx }) => { + const queryBuilder = ctx.queryBuilder; +}); +``` + +// ============================================================================ +// READ Operations +// ============================================================================ + +/** + * Get all records from a table + */ +export async function getAllItems() { + return await queryBuilder + .selectFrom("items") + .select(["id", "name", "status", "createdAt"]) + .orderBy("createdAt", "desc") + .execute(); +} + +/** + * Get a single record by ID + */ +export async function getItemById(id: number) { + return await queryBuilder + .selectFrom("items") + .where("id", "=", id) + .executeTakeFirst(); +} + +// ============================================================================ +// CREATE Operations +// ============================================================================ + +export async function createItem(data: { name: string; status: string }) { + return await queryBuilder.insertInto("items").values(data).executeTakeFirst(); +} + +// ============================================================================ +// UPDATE Operations +// ============================================================================ + +export async function updateItem( + id: number, + data: { name?: string; status?: string }, +) { + return await queryBuilder + .update("items") + .set(data) + .where("id", "=", id) + .execute(); +} + +// ============================================================================ +// DELETE Operations +// ============================================================================ + +export async function deleteItem(id: number) { + return await queryBuilder.deleteFrom("items").where("id", "=", id).execute(); +} +``` + +**Query Builder Patterns:** + +- **Filtering**: `.where("field", "=", value)`, `.where("date", ">=", ["exactDay", "2024-01-01"])` +- **Select specific fields**: `.select(["id", "name", "status"])` +- **Ordering**: `.orderBy("createdAt", "desc")` +- **Single record**: `.executeTakeFirst()` +- **Multiple records**: `.execute()` +- **Array fields**: Use `[value]` for single-select enums +- **Date filters**: Use `["exactDay", date]` format + +Organize functions by domain (e.g., all user-related functions together). + +#### Step 2: Create tRPC API Router + +**File: `apps/server/router.ts`** + +Expose your database functions as type-safe tRPC procedures: + +```typescript +import { z } from "zod"; +import { router, publicProcedure } from "./trpc"; +import * as db from "./taylordb/query-builder"; + +export const appRouter = router({ + // Group by domain/feature + items: { + getAll: publicProcedure.query(async () => { + return await db.getAllItems(); + }), + + getById: publicProcedure + .input(z.object({ id: z.number() })) + .query(async ({ input }) => { + return await db.getItemById(input.id); + }), + + create: publicProcedure + .input( + z.object({ + name: z.string().min(1), + status: z.string(), + }), + ) + .mutation(async ({ input }) => { + return await db.createItem(input); + }), + + update: publicProcedure + .input( + z.object({ + id: z.number(), + name: z.string().optional(), + status: z.string().optional(), + }), + ) + .mutation(async ({ input }) => { + const { id, ...data } = input; + return await db.updateItem(id, data); + }), + + delete: publicProcedure + .input(z.object({ id: z.number() })) + .mutation(async ({ input }) => { + return await db.deleteItem(input.id); + }), + }, +}); + +export type AppRouter = typeof appRouter; +``` + +**Organization:** + +- Group related procedures (e.g., `items`, `users`, `projects`) +- Use Zod for input validation +- Queries for reads, mutations for writes +- Export `AppRouter` type for frontend + +--- + +### **Phase 3: Build the Frontend** + +#### Step 1: Update Design System + +**File: `apps/client/src/index.css`** + +Update the design tokens based on your chosen color scheme: + +```css +@layer base { + :root { + /* Update these HSL values for your color scheme */ + --background: 0 0% 100%; + --foreground: 222.2 84% 4.9%; + --primary: 262 83% 58%; /* Example: Purple */ + --primary-foreground: 210 40% 98%; + --accent: 262 90% 95%; + --accent-foreground: 262 83% 58%; + /* ... customize all tokens ... */ + --radius: 0.75rem; /* Border radius */ + } + + .dark { + /* Dark mode colors */ + --background: 222.2 84% 4.9%; + --foreground: 210 40% 98%; + --primary: 263 70% 65%; + /* ... */ + } +} +``` + +#### Step 2: Create shadcn/ui Components + +**Always use shadcn/ui components**. Install components as needed with: + +```bash +pnpm dlx shadcn@latest add +``` + +For concrete install commands and patterns: + +- See `docs/SHADCN_INSTALLATION.md` for component install snippets +- See `docs/SHADCN_DASHBOARD_PATTERNS.md` for tables, dialogs, forms, toasts, sheets, command palette, etc. + +#### Step 3: Build Page Components + +**File: `apps/client/src/pages/DashboardPage.tsx`** + +Create feature-rich, modern pages: + +```typescript +import { useState } from "react"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { trpc } from "@/lib/trpc"; +import { PlusIcon, Loader2 } from "lucide-react"; + +export default function DashboardPage() { + const [name, setName] = useState(""); + + const { data: items, isLoading, refetch } = trpc.items.getAll.useQuery(); + + const createMutation = trpc.items.create.useMutation({ + onSuccess: () => { + refetch(); + setName(""); + }, + }); + + const deleteMutation = trpc.items.delete.useMutation({ + onSuccess: () => refetch(), + }); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (name) { + createMutation.mutate({ name, status: "active" }); + } + }; + + return ( +
+
+

Dashboard

+

Manage your items

+
+ +
+ {/* Create Form */} + + + Add New Item + + Create a new item in your database + + + +
+
+ + setName(e.target.value)} + placeholder="Enter item name..." + required + /> +
+ +
+
+
+ + {/* Items List */} + + + Your Items + + + {isLoading && ( +
+ +
+ )} + + {items && items.length === 0 && ( +

+ No items yet. Create your first item above. +

+ )} + +
+ {items?.map((item) => ( +
+
+

{item.name}

+

+ {item.status} +

+
+ +
+ ))} +
+
+
+
+
+ ); +} +``` + +#### Step 4: Update Routing + +**File: `apps/client/src/main.tsx`** + +Add your new pages to the router: + +```typescript +import { createBrowserRouter, RouterProvider } from "react-router-dom"; +import App from "./App"; +import HomePage from "./pages/HomePage"; +import DashboardPage from "./pages/DashboardPage"; + +const router = createBrowserRouter([ + { + path: "/", + element: , + children: [ + { index: true, element: }, + { path: "dashboard", element: }, + ], + }, +]); +``` + +**File: `apps/client/src/App.tsx`** + +Update navigation: + +```typescript +const navItems = [ + { to: "/", label: "Home" }, + { to: "/dashboard", label: "Dashboard" }, +]; +``` + +--- + +### **Phase 4: Polish & Validate** + +#### Step 1: Run Type Checking + +**ALWAYS run this before considering your work complete:** + +```bash +pnpm build +``` + +Fix all TypeScript errors. Never use `any` unless absolutely necessary. + +#### Step 2: Run Linter + +```bash +pnpm lint +``` + +Fix all linting errors. + +#### Step 3: Test in Browser + +The dev server should be running. Test: + +- All CRUD operations work correctly +- Data updates in real-time +- Error states display properly +- Loading states show appropriate feedback +- UI is responsive and looks great + +--- + +## 🎨 Design Guidelines + +### Visual Excellence Principles + +1. **No Generic Colors**: Never use plain red/blue/green. Use curated HSL palettes. + - ✅ `hsl(262 83% 58%)` (vibrant purple) + - ❌ `#0000ff` (plain blue) + +2. **Premium Aesthetics**: Make it feel high-end + - Use subtle gradients, shadows, and glassmorphism + - Add smooth transitions (`transition-all duration-200`) + - Implement hover states on interactive elements + - Use proper spacing and visual hierarchy + +3. **Modern Typography**: + - Import Google Fonts (e.g., Inter, Outfit, Manrope) + - Use varied font weights (400, 500, 600, 700) + - Proper text sizing hierarchy + +4. **Micro-Animations**: + - Loading spinners with `lucide-react` icons + `animate-spin` + - Fade-ins on data load + - Smooth transitions on hover + - Button press feedback + +5. **Dashboard-First Design**: + - Card-based layouts + - Clear visual grouping + - Stats/metrics prominently displayed + - Intuitive navigation + +### Component Structure + +**Always follow this pattern:** + +```typescript +// 1. Imports (external, then internal) +import { useState } from "react"; +import { Card } from "@/components/ui/card"; +import { trpc } from "@/lib/trpc"; + +// 2. Type definitions (if needed) +interface ItemFormProps { + onSuccess: () => void; +} + +// 3. Component (arrow function) +export default function ItemForm({ onSuccess }: ItemFormProps) { + // 4. State & hooks + const [name, setName] = useState(""); + const createMutation = trpc.items.create.useMutation({ onSuccess }); + + // 5. Event handlers + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + createMutation.mutate({ name }); + }; + + // 6. Render + return
{/* ... */}
; +} +``` + +--- + +## 📁 File Organization Best Practices + +### Backend Structure + +``` +apps/server/ +├── taylordb/ +│ ├── types.ts # Generated types (DO NOT EDIT) +│ └── query-builder.ts # All database operations +├── router.ts # tRPC API routes +├── trpc.ts # tRPC configuration +└── index.ts # Server entry point +``` + +### Frontend Structure + +``` +apps/client/src/ +├── components/ +│ ├── ui/ # shadcn/ui components (auto-generated) +│ └── [custom]/ # Your custom components +├── pages/ +│ └── [PageName].tsx # Route pages +├── lib/ +│ ├── trpc.ts # tRPC client setup +│ └── utils.ts # Utilities (cn helper, etc.) +├── App.tsx # Layout + navigation +├── main.tsx # Router + app initialization +└── index.css # Global styles + design tokens +``` + +### Where to Put What + +| What | Where | +| ---------------------- | -------------------------------------------- | +| Database queries | `apps/server/taylordb/query-builder.ts` | +| API endpoints | `apps/server/router.ts` | +| Route pages | `apps/client/src/pages/` | +| Reusable UI components | `apps/client/src/components/` | +| shadcn/ui components | `apps/client/src/components/ui/` (auto) | +| Design tokens | `apps/client/src/index.css` | +| TypeScript types | Use generated types from `taylordb/types.ts` | + +--- + +## 🔧 TaylorDB Query Builder Reference + +Instead of duplicating examples here, use the dedicated docs: + +- `docs/TAYLORDB_QUERY_REFERENCE.md` – index for all query builder docs +- `docs/TAYLORDB_BASIC_QUERIES.md` – `selectFrom`, filtering, ordering, date filters +- `docs/TAYLORDB_WRITE_OPERATIONS.md` – `insertInto`, `update`, `deleteFrom` +- `docs/TAYLORDB_ADVANCED_PATTERNS.md` – aggregations, totals, conditional queries, pagination +- `docs/TAYLORDB_FIELD_TYPES.md` – field type mapping, nullable handling, enums +- `docs/TAYLORDB_ATTACHMENTS.md` – selecting and writing attachment fields +- `docs/TAYLORDB_PITFALLS_BEST_PRACTICES.md` – pitfalls and best practices + +When writing queries in `apps/server/taylordb/query-builder.ts`, mirror the patterns from these docs and keep everything **strongly typed** using `taylordb/types.ts`. + +--- + +## ✅ Code Style Guidelines + +### TypeScript + +- **Never use `any`**. Use proper types from `taylordb/types.ts` +- Strict null checks: handle `null` and `undefined` explicitly +- Use type inference where obvious, explicit types for function params/returns + +### Naming Conventions + +- **Variables/Functions**: `camelCase` (e.g., `getUserData`) +- **Components**: `PascalCase` (e.g., `DashboardPage`) +- **Constants**: `UPPER_CASE` (e.g., `MAX_ITEMS`) +- **Files**: Match component name (e.g., `DashboardPage.tsx`) + +### Imports + +- Group external imports first, then internal +- Use path aliases: `@/components/...` not `../../components/...` + +### Components + +- Use arrow functions: `const MyComponent = () => { ... }` +- Props typing: `interface MyComponentProps { ... }` +- Keep components focused (single responsibility) + +### Error Handling + +- Display error states in UI +- Use tRPC's built-in error handling +- Show user-friendly messages + +### Comments + +- Use JSDoc for functions: `/** Description */` +- Explain "why", not "what" +- Remove commented-out code + +--- + +## 🎓 Example: Building a Task Manager Dashboard + +**User Request**: "Build a task manager with projects and tasks" + +### 1. Analyze Schema + +Assume TaylorDB has: + +- `projects` table: `id`, `name`, `description`, `status` +- `tasks` table: `id`, `title`, `projectId`, `status`, `dueDate` + +### 2. Design Decision + +- **Color**: Gradient purple/blue theme +- **Style**: Modern with glassmorphism cards +- **Layout**: Projects on left sidebar, tasks on right + +### 3. Backend (`apps/server/taylordb/query-builder.ts`) + +```typescript +export async function getAllProjects() { + return await queryBuilder + .selectFrom("projects") + .select(["id", "name", "description", "status"]) + .execute(); +} + +export async function getTasksByProject(projectId: number) { + return await queryBuilder + .selectFrom("tasks") + .where("projectId", "=", projectId) + .orderBy("dueDate", "asc") + .execute(); +} +``` + +### 4. API (`apps/server/router.ts`) + +```typescript +export const appRouter = router({ + projects: { + getAll: publicProcedure.query(() => db.getAllProjects()), + }, + tasks: { + getByProject: publicProcedure + .input(z.object({ projectId: z.number() })) + .query(({ input }) => db.getTasksByProject(input.projectId)), + }, +}); +``` + +### 5. Frontend (`apps/client/src/pages/TasksPage.tsx`) + +Build the UI with cards, proper loading states, and type-safe tRPC calls. + +--- + +## ⚠️ Critical Rules + +1. **NEVER use mock data.** Always connect to real TaylorDB. +2. **NEVER ignore TypeScript errors.** Fix them before moving on. +3. **ALWAYS use shadcn/ui components** instead of hand-rolling UI. +4. **NEVER modify generated types** in `taylordb/types.ts`. +5. **ALWAYS run `pnpm build`** to validate your work. +6. **Design must be modern and premium**, not basic MVP. + +--- + +## 🎯 Success Criteria + +Your implementation is successful when: + +- ✅ All TypeScript errors are resolved (`pnpm build` passes) +- ✅ All lint errors are fixed (`pnpm lint` passes) +- ✅ UI looks modern and premium (not basic/generic) +- ✅ All CRUD operations work correctly with TaylorDB +- ✅ Loading and error states are handled gracefully +- ✅ Code is well-organized and follows best practices +- ✅ Type safety is maintained from database to UI + +--- + +**Remember**: You're building production-quality applications that should impress users from the first glance. Focus on visual excellence, type safety, and solid architecture. diff --git a/README.md b/README.md new file mode 100644 index 0000000..96d2989 --- /dev/null +++ b/README.md @@ -0,0 +1,223 @@ +# TaylorDB Full-Stack Template + +A production-ready template for building modern, type-safe web applications with TaylorDB. Designed for AI-assisted development with comprehensive documentation and best practices built-in. + +## 🎯 What This Template Provides + +- **Full-Stack Setup**: React frontend + Node.js backend in a monorepo +- **Type Safety**: End-to-end TypeScript from database to UI +- **Modern UI**: shadcn/ui components with Tailwind CSS +- **Type-Safe API**: tRPC for seamless client-server communication +- **TaylorDB Integration**: Query builder with generated types +- **AI-Ready**: Comprehensive documentation for AI-assisted development + +--- + +## 🚀 Quick Start + +### 1. Install Dependencies + +```bash +pnpm install +``` + +--- + +## 📁 Project Structure + +``` +taylordb-fullstack-template/ +├── apps/ +│ ├── client/ # React frontend (Vite) +│ │ ├── src/ +│ │ │ ├── components/ui/ # shadcn/ui components +│ │ │ ├── pages/ # Route pages +│ │ │ ├── lib/ # Utilities & tRPC client +│ │ │ └── index.css # Design tokens +│ │ └── package.json +│ │ +│ └── server/ # Node.js backend +│ ├── taylordb/ +│ │ ├── types.ts # Generated schema types +│ │ └── query-builder.ts # Database operations +│ ├── router.ts # tRPC API routes +│ └── package.json +│ +├── docs/ # Comprehensive guides +│ ├── TAYLORDB_QUERY_REFERENCE.md +│ └── SHADCN_COMPONENTS_GUIDE.md +│ +├── AGENTS.md # AI agent instructions +└── taylordb.yml # Deployment config +``` + +--- + +## 📚 Documentation + +This template includes comprehensive documentation for both human and AI developers: + +### For AI Agents + +- **[AGENTS.md](./AGENTS.md)**: Complete AI agent instructions + - Development workflow (Planning → Execution → Verification) + - Design guidelines for modern UIs + - Code organization best practices + - Type safety patterns + - Example implementations + +### For Developers + +- **[docs/TAYLORDB_QUERY_REFERENCE.md](./docs/TAYLORDB_QUERY_REFERENCE.md)**: Query builder reference + + - All CRUD operations with examples + - Field type handling + - Advanced patterns (aggregations, pagination) + - Common pitfalls and solutions + +- **[docs/SHADCN_COMPONENTS_GUIDE.md](./docs/SHADCN_COMPONENTS_GUIDE.md)**: UI component guide + - Dashboard patterns + - Form examples + - Data tables + - Responsive design tips + +--- + +## 🎨 Tech Stack + +### Frontend + +- **React 19** with TypeScript +- **Vite 7** for fast builds +- **TailwindCSS 4** for styling +- **shadcn/ui** for UI components +- **React Router v6** for routing +- **tRPC React Query** for API calls + +### Backend + +- **Node.js** with TypeScript +- **Express 5** web server +- **tRPC 11** for type-safe APIs +- **Zod** for validation +- **TaylorDB Query Builder** for database + +--- + +## 🎯 Key Features + +### ✅ Full Type Safety + +```typescript +// Backend defines the API +export const appRouter = router({ + users: { + getById: publicProcedure + .input(z.object({ id: z.number() })) + .query(async ({ input }) => { ... }), + }, +}); + +// Frontend gets full autocomplete +const { data: user } = trpc.users.getById.useQuery({ id: 1 }); +// ^? User | undefined (fully typed!) +``` + +### ✅ Modern UI Components + +All components from shadcn/ui with: + +- Full dark mode support +- Responsive design +- Accessible by default +- Customizable with Tailwind + +### ✅ Database Integration + +Type-safe queries with TaylorDB: + +```typescript +// Auto-generated types from your schema +export async function getAllUsers() { + return await queryBuilder + .selectFrom("users") + .select(["id", "name", "email"]) + .execute(); +} +``` + +--- + +## 🤖 AI-Assisted Development + +This template is optimized for AI-assisted development: + +1. **Read AGENTS.md**: Comprehensive instructions for AI agents +2. **Follow the workflow**: Planning → Execution → Verification +3. **Use type safety**: All examples use strict TypeScript +4. **Reference docs**: Query patterns, component examples, best practices + +The AI agent will: + +- Understand your TaylorDB schema +- Design appropriate color schemes +- Build type-safe CRUD operations +- Create modern, responsive UIs +- Follow best practices automatically + +--- + +## 🚢 Deployment + +This template is designed to deploy to TaylorDB's platform using the included `taylordb.yml` configuration. + +**Environment Variables Required:** + +- `TAYLORDB_BASE_URL` +- `TAYLORDB_SERVER_ID` + +--- + +## 📖 Usage Examples + +### 1. Add a New Feature + +1. Create database functions in `apps/server/taylordb/query-builder.ts` +2. Expose via tRPC in `apps/server/router.ts` +3. Build UI in `apps/client/src/pages/` +4. Add route in `apps/client/src/main.tsx` + +### 2. Add shadcn/ui Component + +```bash +pnpm dlx shadcn@latest add +``` + +Example: + +```bash +pnpm dlx shadcn@latest add table dialog toast +``` + +### 3. Customize Design + +Edit `apps/client/src/index.css` to change colors, fonts, and spacing. + +--- + +## 🔗 Resources + +- **shadcn/ui**: https://ui.shadcn.com/ +- **tRPC**: https://trpc.io/ +- **TaylorDB**: https://taylordb.ai/ +- **Tailwind CSS**: https://tailwindcss.com/ + +--- + +## 📄 License + +MIT - Use freely for any project! + +--- + +**Built for modern, type-safe full-stack development with AI assistance** ✨ diff --git a/apps/client/components.json b/apps/client/components.json new file mode 100644 index 0000000..7341eef --- /dev/null +++ b/apps/client/components.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "src/index.css", + "baseColor": "slate", + "cssVariables": true + }, + "aliases": { + "components": "src/components", + "utils": "@/lib/utils" + } +} + diff --git a/apps/client/index.html b/apps/client/index.html new file mode 100644 index 0000000..1d24401 --- /dev/null +++ b/apps/client/index.html @@ -0,0 +1,13 @@ + + + + + + + blank + + +
+ + + diff --git a/apps/client/package.json b/apps/client/package.json new file mode 100644 index 0000000..4e20f32 --- /dev/null +++ b/apps/client/package.json @@ -0,0 +1,54 @@ +{ + "name": "@repo/client", + "private": true, + "version": "0.0.10", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-tabs": "^1.1.13", + "@tanstack/react-query": "^5.90.16", + "@trpc/client": "^11.8.1", + "@trpc/react-query": "^11.8.1", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cors": "^2.8.5", + "express": "^5.2.1", + "lucide-react": "^0.561.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-router-dom": "^6.28.1", + "recharts": "^3.5.0", + "tailwind-merge": "^3.4.0", + "zod": "^4.3.5" + }, + "devDependencies": { + "@types/express": "^5.0.6", + "@repo/server": "workspace:*", + "@taylordb/query-builder": "^0.10.3", + "@eslint/js": "^9.39.1", + "@tailwindcss/vite": "^4.1.18", + "@types/node": "^24.10.1", + "@types/react": "^19.2.2", + "@types/react-dom": "^19.2.2", + "@vitejs/plugin-react": "^5.1.0", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "tailwindcss": "^4.1.18", + "tw-animate-css": "^1.4.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.46.3", + "vite": "^7.2.2" + } +} diff --git a/apps/client/public/vite.svg b/apps/client/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/apps/client/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/client/src/App.tsx b/apps/client/src/App.tsx new file mode 100644 index 0000000..516804c --- /dev/null +++ b/apps/client/src/App.tsx @@ -0,0 +1,129 @@ +import { Moon, Sun, Database, Palette } from "lucide-react"; +import { useEffect, useState } from "react"; +import { Outlet } from "react-router-dom"; + +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; + +const themes = [ + { id: "purple", name: "Purple", color: "hsl(270 80% 60%)" }, + { id: "ocean", name: "Ocean", color: "hsl(210 90% 55%)" }, + { id: "sunset", name: "Sunset", color: "hsl(25 95% 55%)" }, + { id: "forest", name: "Forest", color: "hsl(145 70% 40%)" }, + { id: "rose", name: "Rose", color: "hsl(350 80% 60%)" }, + { id: "slate", name: "Slate", color: "hsl(220 15% 35%)" }, +] as const; + +type ThemeId = (typeof themes)[number]["id"]; +type Mode = "light" | "dark"; + +const getInitialTheme = (): ThemeId => { + if (typeof window === "undefined") return "purple"; + const stored = localStorage.getItem("color-theme"); + if (themes.some((t) => t.id === stored)) return stored as ThemeId; + return "purple"; +}; + +const getInitialMode = (): Mode => { + if (typeof window === "undefined") return "light"; + const stored = localStorage.getItem("mode"); + if (stored === "light" || stored === "dark") return stored; + return window.matchMedia("(prefers-color-scheme: dark)").matches + ? "dark" + : "light"; +}; + +function App() { + const [theme, setTheme] = useState(getInitialTheme); + const [mode, setMode] = useState(getInitialMode); + + useEffect(() => { + const root = document.documentElement; + + // Remove existing theme classes + themes.forEach((t) => root.classList.remove(`theme-${t.id}`)); + + // Add current theme class + root.classList.add(`theme-${theme}`); + root.classList.toggle("dark", mode === "dark"); + + localStorage.setItem("color-theme", theme); + localStorage.setItem("mode", mode); + }, [theme, mode]); + + return ( +
+
+
+
+
+ +
+ TaylorDB +
+ +
+ {/* Theme Picker */} + + + + + + {themes.map((t) => ( + setTheme(t.id)} + className="flex items-center gap-3 cursor-pointer" + > +
+ + {t.name} + + {theme === t.id && ( + + )} + + ))} + + + + {/* Dark/Light Mode Toggle */} + +
+
+
+
+ +
+
+ ); +} + +export default App; diff --git a/apps/client/src/assets/react.svg b/apps/client/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/apps/client/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/client/src/components/demo/Avatar.tsx b/apps/client/src/components/demo/Avatar.tsx new file mode 100644 index 0000000..b847ef7 --- /dev/null +++ b/apps/client/src/components/demo/Avatar.tsx @@ -0,0 +1,11 @@ +interface AvatarProps { + name: string; +} + +export function Avatar({ name }: AvatarProps) { + return ( +
+ {name.charAt(0).toUpperCase()} +
+ ); +} diff --git a/apps/client/src/components/demo/CodePreview.tsx b/apps/client/src/components/demo/CodePreview.tsx new file mode 100644 index 0000000..d021781 --- /dev/null +++ b/apps/client/src/components/demo/CodePreview.tsx @@ -0,0 +1,14 @@ +interface CodePreviewProps { + data: unknown; +} + +export function CodePreview({ data }: CodePreviewProps) { + return ( +
+
+
+        {JSON.stringify(data, null, 2)}
+      
+
+ ); +} diff --git a/apps/client/src/components/demo/DemoCard.tsx b/apps/client/src/components/demo/DemoCard.tsx new file mode 100644 index 0000000..87f549f --- /dev/null +++ b/apps/client/src/components/demo/DemoCard.tsx @@ -0,0 +1,43 @@ +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import type { LucideIcon } from "lucide-react"; + +interface DemoCardProps { + title: string; + description: string; + icon: LucideIcon; + iconColorClass?: string; + glowClass?: string; + children: React.ReactNode; +} + +export function DemoCard({ + title, + description, + icon: Icon, + iconColorClass = "bg-primary/10 text-primary", + glowClass = "glow-primary", + children, +}: DemoCardProps) { + return ( + + +
+
+ +
+
+ {title} + {description} +
+
+
+ {children} +
+ ); +} diff --git a/apps/client/src/components/demo/EmptyState.tsx b/apps/client/src/components/demo/EmptyState.tsx new file mode 100644 index 0000000..2144fd2 --- /dev/null +++ b/apps/client/src/components/demo/EmptyState.tsx @@ -0,0 +1,7 @@ +interface EmptyStateProps { + message: string; +} + +export function EmptyState({ message }: EmptyStateProps) { + return

{message}

; +} diff --git a/apps/client/src/components/demo/ItemRow.tsx b/apps/client/src/components/demo/ItemRow.tsx new file mode 100644 index 0000000..7263a22 --- /dev/null +++ b/apps/client/src/components/demo/ItemRow.tsx @@ -0,0 +1,14 @@ +interface ItemRowProps { + children: React.ReactNode; + className?: string; +} + +export function ItemRow({ children, className = "" }: ItemRowProps) { + return ( +
+ {children} +
+ ); +} diff --git a/apps/client/src/components/demo/LoadingSpinner.tsx b/apps/client/src/components/demo/LoadingSpinner.tsx new file mode 100644 index 0000000..a4788f1 --- /dev/null +++ b/apps/client/src/components/demo/LoadingSpinner.tsx @@ -0,0 +1,31 @@ +import { Loader2 } from "lucide-react"; + +interface LoadingSpinnerProps { + size?: "sm" | "md" | "lg"; + colorClass?: string; + className?: string; +} + +const sizeClasses = { + sm: "h-4 w-4", + md: "h-6 w-6", + lg: "h-8 w-8", +}; + +export function LoadingSpinner({ + size = "lg", + colorClass = "text-primary", + className = "", +}: LoadingSpinnerProps) { + return ( +
+ +
+ ); +} + +export function InlineSpinner({ size = "sm" }: { size?: "sm" | "md" }) { + return ; +} diff --git a/apps/client/src/components/demo/StatusBadge.tsx b/apps/client/src/components/demo/StatusBadge.tsx new file mode 100644 index 0000000..98d08db --- /dev/null +++ b/apps/client/src/components/demo/StatusBadge.tsx @@ -0,0 +1,20 @@ +interface StatusBadgeProps { + status: "success" | "warning" | "info"; + children: React.ReactNode; +} + +const badgeClasses = { + success: "badge-success", + warning: "badge-warning", + info: "bg-blue-500/10 text-blue-500", +}; + +export function StatusBadge({ status, children }: StatusBadgeProps) { + return ( + + {children} + + ); +} diff --git a/apps/client/src/components/demo/examples/FileUploadExample.tsx b/apps/client/src/components/demo/examples/FileUploadExample.tsx new file mode 100644 index 0000000..53c1802 --- /dev/null +++ b/apps/client/src/components/demo/examples/FileUploadExample.tsx @@ -0,0 +1,196 @@ +import { useState, useRef } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Upload, X, FileIcon, CheckCircle2, Send } from "lucide-react"; +import { DemoCard, InlineSpinner } from "@/components/demo"; +import { trpc } from "@/lib/trpc"; + +function FileField({ + label, + file, + onSelect, + onClear, +}: { + label: string; + file: File | null; + onSelect: (file: File) => void; + onClear: () => void; +}) { + const inputRef = useRef(null); + + const formatSize = (bytes: number) => { + if (bytes === 0) return "0 B"; + const units = ["B", "KB", "MB", "GB"]; + const i = Math.floor(Math.log(bytes) / Math.log(1024)); + return `${(bytes / Math.pow(1024, i)).toFixed(i === 0 ? 0 : 1)} ${units[i]}`; + }; + + return ( +
+

+ {label} +

+ {file ? ( +
+ +
+

{file.name}

+

+ {file.type || "unknown"} • {formatSize(file.size)} +

+
+ +
+ ) : ( +
inputRef.current?.click()} + > + +

+ Click to upload{" "} + {label.toLowerCase()} +

+ { + const f = e.target.files?.[0]; + if (f) onSelect(f); + e.target.value = ""; + }} + /> +
+ )} +
+ ); +} + +export function FileUploadExample() { + const [name, setName] = useState(""); + const [email, setEmail] = useState(""); + const [cv, setCv] = useState(null); + const [profileImage, setProfileImage] = useState(null); + + const submitMutation = trpc.submitUserData.submit.useMutation(); + + const handleSubmit = async () => { + const formData = new FormData(); + formData.append("name", name); + formData.append("email", email); + if (cv) formData.append("cv", cv); + if (profileImage) formData.append("profileImage", profileImage); + + await submitMutation.mutateAsync(formData); + + setName(""); + setEmail(""); + setCv(null); + setProfileImage(null); + }; + + const canSubmit = name.trim() && email.trim() && !submitMutation.isPending; + + return ( + + {/* Text Fields */} +
+ setName(e.target.value)} + placeholder="Name" + className="flex-1" + /> + setEmail(e.target.value)} + placeholder="Email" + type="email" + className="flex-1" + /> +
+ + {/* Named File Fields */} + setCv(null)} + /> + setProfileImage(null)} + /> + + {/* Submit */} + + + {/* Error */} + {submitMutation.error && ( +
+ {submitMutation.error.message} +
+ )} + + {/* Last Result */} + {submitMutation.data && ( +
+
+ + + {submitMutation.data.received.name} + + + {submitMutation.data.received.email} + +
+
+ {( + [ + ["cv", submitMutation.data.received.cv], + ["profileImage", submitMutation.data.received.profileImage], + ] as const + ) + .filter(([, f]) => f !== null) + .map(([key, f]) => ( + + + {key}: {f!.name} + + ))} +
+
+ )} +
+ ); +} diff --git a/apps/client/src/components/demo/examples/HelloExample.tsx b/apps/client/src/components/demo/examples/HelloExample.tsx new file mode 100644 index 0000000..f60866d --- /dev/null +++ b/apps/client/src/components/demo/examples/HelloExample.tsx @@ -0,0 +1,48 @@ +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { trpc } from "@/lib/trpc"; +import { Sparkles, Zap } from "lucide-react"; +import { DemoCard, InlineSpinner, CodePreview } from "@/components/demo"; + +export function HelloExample() { + const [name, setName] = useState(""); + const { data, isLoading, refetch } = trpc.hello.useQuery( + { name: name || undefined }, + { enabled: false } + ); + + return ( + +
+ setName(e.target.value)} + placeholder="Enter your name..." + className="flex-1" + /> + +
+ {data && } +
+ ); +} diff --git a/apps/client/src/components/demo/examples/PostsExample.tsx b/apps/client/src/components/demo/examples/PostsExample.tsx new file mode 100644 index 0000000..d0c9427 --- /dev/null +++ b/apps/client/src/components/demo/examples/PostsExample.tsx @@ -0,0 +1,209 @@ +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { trpc } from "@/lib/trpc"; +import { FileText, Plus, Sparkles, Trash2 } from "lucide-react"; +import { + DemoCard, + LoadingSpinner, + InlineSpinner, + EmptyState, + StatusBadge, +} from "@/components/demo"; + +interface Post { + id: number; + title: string; + content: string | null; + published: boolean; +} + +export function PostsExample() { + const [title, setTitle] = useState(""); + const [content, setContent] = useState(""); + const [authorId, setAuthorId] = useState("1"); + + const utils = trpc.useUtils(); + const { data: posts, isLoading } = trpc.posts.getAll.useQuery(); + + const createMutation = trpc.posts.create.useMutation({ + onSuccess: () => { + utils.posts.getAll.invalidate(); + setTitle(""); + setContent(""); + }, + }); + + const publishMutation = trpc.posts.publish.useMutation({ + onSuccess: () => utils.posts.getAll.invalidate(), + }); + + const deleteMutation = trpc.posts.delete.useMutation({ + onSuccess: () => utils.posts.getAll.invalidate(), + }); + + const handleCreate = () => { + createMutation.mutate({ + title, + content, + authorId: parseInt(authorId), + published: false, + }); + }; + + return ( + + {/* Create Form */} + + + {/* Posts List */} + {isLoading ? ( + + ) : ( +
+ {posts?.length === 0 && ( + + )} + {posts?.map((post) => ( + publishMutation.mutate({ id: post.id })} + onDelete={() => deleteMutation.mutate({ id: post.id })} + /> + ))} +
+ )} +
+ ); +} + +// ============================================================================ +// Sub-components +// ============================================================================ + +interface CreatePostFormProps { + title: string; + content: string; + authorId: string; + onTitleChange: (value: string) => void; + onContentChange: (value: string) => void; + onAuthorIdChange: (value: string) => void; + onSubmit: () => void; + isLoading: boolean; +} + +function CreatePostForm({ + title, + content, + authorId, + onTitleChange, + onContentChange, + onAuthorIdChange, + onSubmit, + isLoading, +}: CreatePostFormProps) { + return ( +
+
+ onTitleChange(e.target.value)} + placeholder="Post title..." + className="flex-1" + /> +
+ + onAuthorIdChange(e.target.value)} + type="number" + /> +
+
+
+ onContentChange(e.target.value)} + placeholder="Write something amazing..." + className="flex-1" + /> + +
+
+ ); +} + +interface PostCardProps { + post: Post; + onPublish: () => void; + onDelete: () => void; +} + +function PostCard({ post, onPublish, onDelete }: PostCardProps) { + return ( +
+
+
+
+

{post.title}

+ + {post.published ? "Published" : "Draft"} + +
+

+ {post.content} +

+
+
+ {!post.published && ( + + )} + +
+
+
+ ); +} diff --git a/apps/client/src/components/demo/examples/UsersExample.tsx b/apps/client/src/components/demo/examples/UsersExample.tsx new file mode 100644 index 0000000..30f23a1 --- /dev/null +++ b/apps/client/src/components/demo/examples/UsersExample.tsx @@ -0,0 +1,241 @@ +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { trpc } from "@/lib/trpc"; +import { Users, Plus, Edit2, Check, X, Trash2 } from "lucide-react"; +import { + DemoCard, + LoadingSpinner, + InlineSpinner, + EmptyState, + ItemRow, + Avatar, +} from "@/components/demo"; + +interface User { + id: number; + name: string; + email: string; +} + +export function UsersExample() { + const [newName, setNewName] = useState(""); + const [newEmail, setNewEmail] = useState(""); + const [editingId, setEditingId] = useState(null); + const [editName, setEditName] = useState(""); + const [editEmail, setEditEmail] = useState(""); + + const utils = trpc.useUtils(); + const { data: users, isLoading } = trpc.users.getAll.useQuery(); + + const createMutation = trpc.users.create.useMutation({ + onSuccess: () => { + utils.users.getAll.invalidate(); + setNewName(""); + setNewEmail(""); + }, + }); + + const updateMutation = trpc.users.update.useMutation({ + onSuccess: () => { + utils.users.getAll.invalidate(); + setEditingId(null); + }, + }); + + const deleteMutation = trpc.users.delete.useMutation({ + onSuccess: () => utils.users.getAll.invalidate(), + }); + + const startEditing = (user: User) => { + setEditingId(user.id); + setEditName(user.name); + setEditEmail(user.email); + }; + + const handleCreate = () => { + createMutation.mutate({ name: newName, email: newEmail }); + }; + + const handleUpdate = (id: number) => { + updateMutation.mutate({ id, name: editName, email: editEmail }); + }; + + return ( + + {/* Create Form */} + + + {/* Users List */} + {isLoading ? ( + + ) : ( +
+ {users?.length === 0 && ( + + )} + {users?.map((user) => ( + startEditing(user)} + onCancelEdit={() => setEditingId(null)} + onSaveEdit={() => handleUpdate(user.id)} + onDelete={() => deleteMutation.mutate({ id: user.id })} + /> + ))} +
+ )} +
+ ); +} + +// ============================================================================ +// Sub-components +// ============================================================================ + +interface CreateUserFormProps { + name: string; + email: string; + onNameChange: (value: string) => void; + onEmailChange: (value: string) => void; + onSubmit: () => void; + isLoading: boolean; +} + +function CreateUserForm({ + name, + email, + onNameChange, + onEmailChange, + onSubmit, + isLoading, +}: CreateUserFormProps) { + return ( +
+ onNameChange(e.target.value)} + placeholder="Name" + className="flex-1" + /> + onEmailChange(e.target.value)} + placeholder="Email" + className="flex-1" + /> + +
+ ); +} + +interface UserRowProps { + user: User; + isEditing: boolean; + editName: string; + editEmail: string; + onEditNameChange: (value: string) => void; + onEditEmailChange: (value: string) => void; + onStartEdit: () => void; + onCancelEdit: () => void; + onSaveEdit: () => void; + onDelete: () => void; +} + +function UserRow({ + user, + isEditing, + editName, + editEmail, + onEditNameChange, + onEditEmailChange, + onStartEdit, + onCancelEdit, + onSaveEdit, + onDelete, +}: UserRowProps) { + if (isEditing) { + return ( + + onEditNameChange(e.target.value)} + className="flex-1" + /> + onEditEmailChange(e.target.value)} + className="flex-1" + /> + + + + ); + } + + return ( + + +
+
{user.name}
+
{user.email}
+
+ + +
+ ); +} diff --git a/apps/client/src/components/demo/examples/index.ts b/apps/client/src/components/demo/examples/index.ts new file mode 100644 index 0000000..1407755 --- /dev/null +++ b/apps/client/src/components/demo/examples/index.ts @@ -0,0 +1,4 @@ +export { HelloExample } from "./HelloExample"; +export { UsersExample } from "./UsersExample"; +export { PostsExample } from "./PostsExample"; +export { FileUploadExample } from "./FileUploadExample"; diff --git a/apps/client/src/components/demo/index.ts b/apps/client/src/components/demo/index.ts new file mode 100644 index 0000000..92c494c --- /dev/null +++ b/apps/client/src/components/demo/index.ts @@ -0,0 +1,7 @@ +export { DemoCard } from "./DemoCard"; +export { LoadingSpinner, InlineSpinner } from "./LoadingSpinner"; +export { EmptyState } from "./EmptyState"; +export { ItemRow } from "./ItemRow"; +export { Avatar } from "./Avatar"; +export { StatusBadge } from "./StatusBadge"; +export { CodePreview } from "./CodePreview"; diff --git a/apps/client/src/components/ui/alert.tsx b/apps/client/src/components/ui/alert.tsx new file mode 100644 index 0000000..41fa7e0 --- /dev/null +++ b/apps/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/apps/client/src/components/ui/button.tsx b/apps/client/src/components/ui/button.tsx new file mode 100644 index 0000000..bcc5eb6 --- /dev/null +++ b/apps/client/src/components/ui/button.tsx @@ -0,0 +1,57 @@ +/* eslint-disable react-refresh/only-export-components */ +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-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 ring-offset-background", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "bg-destructive text-destructive-foreground hover:bg-destructive/90", + outline: + "border border-input bg-background hover:bg-accent hover:text-accent-foreground", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-10 px-4 py-2", + sm: "h-9 rounded-md px-3", + lg: "h-11 rounded-md px-8", + icon: "h-10 w-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +); + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean; +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button"; + return ( + + ); + } +); +Button.displayName = "Button"; + +export { Button, buttonVariants }; diff --git a/apps/client/src/components/ui/card.tsx b/apps/client/src/components/ui/card.tsx new file mode 100644 index 0000000..f62edea --- /dev/null +++ b/apps/client/src/components/ui/card.tsx @@ -0,0 +1,79 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +Card.displayName = "Card" + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardHeader.displayName = "CardHeader" + +const CardTitle = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardTitle.displayName = "CardTitle" + +const CardDescription = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardDescription.displayName = "CardDescription" + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardContent.displayName = "CardContent" + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardFooter.displayName = "CardFooter" + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/apps/client/src/components/ui/dropdown-menu.tsx b/apps/client/src/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000..f5a07f9 --- /dev/null +++ b/apps/client/src/components/ui/dropdown-menu.tsx @@ -0,0 +1,198 @@ +import * as React from "react"; +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; +import { Check, ChevronRight, Circle } from "lucide-react"; + +import { cn } from "@/lib/utils"; + +const DropdownMenu = DropdownMenuPrimitive.Root; + +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; + +const DropdownMenuGroup = DropdownMenuPrimitive.Group; + +const DropdownMenuPortal = DropdownMenuPrimitive.Portal; + +const DropdownMenuSub = DropdownMenuPrimitive.Sub; + +const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup; + +const DropdownMenuSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)); +DropdownMenuSubTrigger.displayName = + DropdownMenuPrimitive.SubTrigger.displayName; + +const DropdownMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DropdownMenuSubContent.displayName = + DropdownMenuPrimitive.SubContent.displayName; + +const DropdownMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)); +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName; + +const DropdownMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, ...props }, ref) => ( + +)); +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName; + +const DropdownMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)); +DropdownMenuCheckboxItem.displayName = + DropdownMenuPrimitive.CheckboxItem.displayName; + +const DropdownMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)); +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName; + +const DropdownMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, ...props }, ref) => ( + +)); +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName; + +const DropdownMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName; + +const DropdownMenuShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ); +}; +DropdownMenuShortcut.displayName = "DropdownMenuShortcut"; + +export { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuGroup, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuRadioGroup, +}; diff --git a/apps/client/src/components/ui/input.tsx b/apps/client/src/components/ui/input.tsx new file mode 100644 index 0000000..68551b9 --- /dev/null +++ b/apps/client/src/components/ui/input.tsx @@ -0,0 +1,22 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Input = React.forwardRef>( + ({ className, type, ...props }, ref) => { + return ( + + ) + } +) +Input.displayName = "Input" + +export { Input } diff --git a/apps/client/src/components/ui/label.tsx b/apps/client/src/components/ui/label.tsx new file mode 100644 index 0000000..683faa7 --- /dev/null +++ b/apps/client/src/components/ui/label.tsx @@ -0,0 +1,24 @@ +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const labelVariants = cva( + "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" +) + +const Label = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, ...props }, ref) => ( + +)) +Label.displayName = LabelPrimitive.Root.displayName + +export { Label } diff --git a/apps/client/src/components/ui/select.tsx b/apps/client/src/components/ui/select.tsx new file mode 100644 index 0000000..d826bdb --- /dev/null +++ b/apps/client/src/components/ui/select.tsx @@ -0,0 +1,158 @@ +import * as React from "react" +import * as SelectPrimitive from "@radix-ui/react-select" +import { Check, ChevronDown, ChevronUp } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Select = SelectPrimitive.Root + +const SelectGroup = SelectPrimitive.Group + +const SelectValue = SelectPrimitive.Value + +const SelectTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + span]:line-clamp-1", + className + )} + {...props} + > + {children} + + + + +)) +SelectTrigger.displayName = SelectPrimitive.Trigger.displayName + +const SelectScrollUpButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName + +const SelectScrollDownButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +SelectScrollDownButton.displayName = + SelectPrimitive.ScrollDownButton.displayName + +const SelectContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, position = "popper", ...props }, ref) => ( + + + + + {children} + + + + +)) +SelectContent.displayName = SelectPrimitive.Content.displayName + +const SelectLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SelectLabel.displayName = SelectPrimitive.Label.displayName + +const SelectItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + + {children} + +)) +SelectItem.displayName = SelectPrimitive.Item.displayName + +const SelectSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SelectSeparator.displayName = SelectPrimitive.Separator.displayName + +export { + Select, + SelectGroup, + SelectValue, + SelectTrigger, + SelectContent, + SelectLabel, + SelectItem, + SelectSeparator, + SelectScrollUpButton, + SelectScrollDownButton, +} diff --git a/apps/client/src/components/ui/tabs.tsx b/apps/client/src/components/ui/tabs.tsx new file mode 100644 index 0000000..26eb109 --- /dev/null +++ b/apps/client/src/components/ui/tabs.tsx @@ -0,0 +1,55 @@ +"use client" + +import * as React from "react" +import * as TabsPrimitive from "@radix-ui/react-tabs" + +import { cn } from "@/lib/utils" + +const Tabs = TabsPrimitive.Root + +const TabsList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsList.displayName = TabsPrimitive.List.displayName + +const TabsTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName + +const TabsContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsContent.displayName = TabsPrimitive.Content.displayName + +export { Tabs, TabsList, TabsTrigger, TabsContent } diff --git a/apps/client/src/components/ui/textarea.tsx b/apps/client/src/components/ui/textarea.tsx new file mode 100644 index 0000000..4d858bb --- /dev/null +++ b/apps/client/src/components/ui/textarea.tsx @@ -0,0 +1,22 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Textarea = React.forwardRef< + HTMLTextAreaElement, + React.ComponentProps<"textarea"> +>(({ className, ...props }, ref) => { + return ( +