Application setup

This commit is contained in:
Bamboo 2026-05-12 22:18:40 +05:00
commit 2feb35a116
70 changed files with 11575 additions and 0 deletions

31
.gitignore vendored Normal file
View File

@ -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

5
.ignore Normal file
View File

@ -0,0 +1,5 @@
.env
.env.*
QUERY_BUILDER.md
.opencode
.ignore

733
AGENTS.md Normal file
View File

@ -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 <component-name>
```
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 (
<div className="container mx-auto p-8 max-w-6xl">
<div className="mb-8">
<h1 className="text-4xl font-bold mb-2">Dashboard</h1>
<p className="text-muted-foreground">Manage your items</p>
</div>
<div className="grid gap-6">
{/* Create Form */}
<Card>
<CardHeader>
<CardTitle>Add New Item</CardTitle>
<CardDescription>
Create a new item in your database
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="flex gap-4">
<div className="flex-1">
<Label htmlFor="name">Item Name</Label>
<Input
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Enter item name..."
required
/>
</div>
<Button
type="submit"
disabled={createMutation.isPending}
className="mt-auto"
>
{createMutation.isPending ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Adding...
</>
) : (
<>
<PlusIcon className="mr-2 h-4 w-4" />
Add Item
</>
)}
</Button>
</form>
</CardContent>
</Card>
{/* Items List */}
<Card>
<CardHeader>
<CardTitle>Your Items</CardTitle>
</CardHeader>
<CardContent>
{isLoading && (
<div className="flex items-center justify-center py-8">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
)}
{items && items.length === 0 && (
<p className="text-center text-muted-foreground py-8">
No items yet. Create your first item above.
</p>
)}
<div className="space-y-3">
{items?.map((item) => (
<div
key={item.id}
className="flex items-center justify-between p-4 border rounded-lg hover:bg-accent transition-colors"
>
<div>
<p className="font-medium">{item.name}</p>
<p className="text-sm text-muted-foreground">
{item.status}
</p>
</div>
<Button
variant="destructive"
size="sm"
onClick={() =>
item.id && deleteMutation.mutate({ id: item.id })
}
disabled={deleteMutation.isPending}
>
Delete
</Button>
</div>
))}
</div>
</CardContent>
</Card>
</div>
</div>
);
}
```
#### 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: <App />,
children: [
{ index: true, element: <HomePage /> },
{ path: "dashboard", element: <DashboardPage /> },
],
},
]);
```
**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 <form onSubmit={handleSubmit}>{/* ... */}</form>;
}
```
---
## 📁 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.

223
README.md Normal file
View File

@ -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 <component-name>
```
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** ✨

View File

@ -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"
}
}

13
apps/client/index.html Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>blank</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

54
apps/client/package.json Normal file
View File

@ -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"
}
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

129
apps/client/src/App.tsx Normal file
View File

@ -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<ThemeId>(getInitialTheme);
const [mode, setMode] = useState<Mode>(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 (
<div className="min-h-screen bg-background text-foreground">
<header className="sticky top-0 z-50 glass-card border-b border-border/50">
<div className="container flex h-16 items-center justify-between">
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-gradient-to-br from-primary to-accent">
<Database className="h-5 w-5 text-white" />
</div>
<span className="text-lg font-bold gradient-text">TaylorDB</span>
</div>
<div className="flex items-center gap-2">
{/* Theme Picker */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
size="icon"
className="rounded-full"
aria-label="Choose theme"
>
<Palette className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-40">
{themes.map((t) => (
<DropdownMenuItem
key={t.id}
onClick={() => setTheme(t.id)}
className="flex items-center gap-3 cursor-pointer"
>
<div
className="w-4 h-4 rounded-full shrink-0"
style={{ backgroundColor: t.color }}
/>
<span className={theme === t.id ? "font-medium" : ""}>
{t.name}
</span>
{theme === t.id && (
<span className="ml-auto text-xs text-primary"></span>
)}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
{/* Dark/Light Mode Toggle */}
<Button
variant="outline"
size="icon"
aria-label="Toggle dark mode"
className="rounded-full"
onClick={() => setMode((m) => (m === "dark" ? "light" : "dark"))}
>
{mode === "dark" ? (
<Sun className="h-4 w-4" />
) : (
<Moon className="h-4 w-4" />
)}
</Button>
</div>
</div>
</header>
<main>
<Outlet />
</main>
</div>
);
}
export default App;

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -0,0 +1,11 @@
interface AvatarProps {
name: string;
}
export function Avatar({ name }: AvatarProps) {
return (
<div className="w-8 h-8 rounded-full bg-gradient-to-br from-primary to-accent flex items-center justify-center text-white text-sm font-medium">
{name.charAt(0).toUpperCase()}
</div>
);
}

View File

@ -0,0 +1,14 @@
interface CodePreviewProps {
data: unknown;
}
export function CodePreview({ data }: CodePreviewProps) {
return (
<div className="relative">
<div className="absolute inset-0 bg-gradient-to-r from-primary/5 via-secondary/5 to-accent/5 rounded-lg" />
<pre className="relative p-4 rounded-lg text-sm font-mono overflow-x-auto">
{JSON.stringify(data, null, 2)}
</pre>
</div>
);
}

View File

@ -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 (
<Card className="card-hover glass-card overflow-hidden">
<CardHeader className="pb-4">
<div className="flex items-center gap-3">
<div className={`p-2 rounded-lg ${iconColorClass} ${glowClass}`}>
<Icon className="h-5 w-5" />
</div>
<div>
<CardTitle className="text-xl">{title}</CardTitle>
<CardDescription>{description}</CardDescription>
</div>
</div>
</CardHeader>
<CardContent className="space-y-4">{children}</CardContent>
</Card>
);
}

View File

@ -0,0 +1,7 @@
interface EmptyStateProps {
message: string;
}
export function EmptyState({ message }: EmptyStateProps) {
return <p className="text-center text-muted-foreground py-4">{message}</p>;
}

View File

@ -0,0 +1,14 @@
interface ItemRowProps {
children: React.ReactNode;
className?: string;
}
export function ItemRow({ children, className = "" }: ItemRowProps) {
return (
<div
className={`item-row flex items-center gap-3 p-4 rounded-lg bg-muted/30 border border-border/50 ${className}`}
>
{children}
</div>
);
}

View File

@ -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 (
<div className={`flex justify-center py-8 ${className}`}>
<Loader2
className={`animate-spin pulse-glow ${sizeClasses[size]} ${colorClass}`}
/>
</div>
);
}
export function InlineSpinner({ size = "sm" }: { size?: "sm" | "md" }) {
return <Loader2 className={`animate-spin ${sizeClasses[size]}`} />;
}

View File

@ -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 (
<span
className={`text-xs px-2 py-0.5 rounded-full font-medium ${badgeClasses[status]}`}
>
{children}
</span>
);
}

View File

@ -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<HTMLInputElement>(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 (
<div className="space-y-1.5">
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
{label}
</p>
{file ? (
<div className="flex items-center gap-3 p-3 rounded-lg bg-muted/30 border border-border/50">
<FileIcon className="h-4 w-4 text-muted-foreground shrink-0" />
<div className="flex-1 min-w-0">
<p className="text-sm font-medium truncate">{file.name}</p>
<p className="text-xs text-muted-foreground">
{file.type || "unknown"} {formatSize(file.size)}
</p>
</div>
<Button
size="icon"
variant="ghost"
className="h-7 w-7 text-muted-foreground hover:text-destructive shrink-0"
onClick={onClear}
>
<X className="h-3.5 w-3.5" />
</Button>
</div>
) : (
<div
className="flex items-center gap-3 p-3 rounded-lg border-2 border-dashed border-border/60 hover:border-primary/40 hover:bg-muted/20 cursor-pointer transition-colors"
onClick={() => inputRef.current?.click()}
>
<Upload className="h-4 w-4 text-muted-foreground/60 shrink-0" />
<p className="text-sm text-muted-foreground">
<span className="font-medium text-foreground">Click to upload</span>{" "}
{label.toLowerCase()}
</p>
<input
ref={inputRef}
type="file"
className="hidden"
onChange={(e) => {
const f = e.target.files?.[0];
if (f) onSelect(f);
e.target.value = "";
}}
/>
</div>
)}
</div>
);
}
export function FileUploadExample() {
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [cv, setCv] = useState<File | null>(null);
const [profileImage, setProfileImage] = useState<File | null>(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 (
<DemoCard
title="Submit User Data"
description="Submit name, email, and files in one tRPC call via multipart/form-data"
icon={Send}
iconColorClass="bg-amber-500/10 text-amber-500"
glowClass="glow-primary"
>
{/* Text Fields */}
<div className="flex gap-3">
<Input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Name"
className="flex-1"
/>
<Input
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
type="email"
className="flex-1"
/>
</div>
{/* Named File Fields */}
<FileField
label="CV"
file={cv}
onSelect={setCv}
onClear={() => setCv(null)}
/>
<FileField
label="Profile Picture"
file={profileImage}
onSelect={setProfileImage}
onClear={() => setProfileImage(null)}
/>
{/* Submit */}
<Button onClick={handleSubmit} disabled={!canSubmit} className="w-full">
{submitMutation.isPending ? (
<>
<InlineSpinner /> Submitting...
</>
) : (
<>
<Send className="h-4 w-4 mr-2" />
Submit
</>
)}
</Button>
{/* Error */}
{submitMutation.error && (
<div className="p-3 rounded-lg bg-destructive/10 border border-destructive/30 text-sm text-destructive">
{submitMutation.error.message}
</div>
)}
{/* Last Result */}
{submitMutation.data && (
<div className="p-3 rounded-lg bg-muted/30 border border-border/50 space-y-2">
<div className="flex items-center gap-2">
<CheckCircle2 className="h-3.5 w-3.5 text-green-500 shrink-0" />
<span className="text-sm font-medium">
{submitMutation.data.received.name}
</span>
<span className="text-xs text-muted-foreground">
{submitMutation.data.received.email}
</span>
</div>
<div className="flex flex-wrap gap-1 ml-5">
{(
[
["cv", submitMutation.data.received.cv],
["profileImage", submitMutation.data.received.profileImage],
] as const
)
.filter(([, f]) => f !== null)
.map(([key, f]) => (
<span
key={key}
className="inline-flex items-center gap-1 text-xs px-2 py-0.5 rounded-full bg-primary/10 text-primary"
>
<FileIcon className="h-3 w-3" />
{key}: {f!.name}
</span>
))}
</div>
</div>
)}
</DemoCard>
);
}

View File

@ -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 (
<DemoCard
title="Hello Query"
description="Simple query to test the connection"
icon={Zap}
iconColorClass="bg-primary/10 text-primary"
glowClass="glow-primary"
>
<div className="flex gap-3">
<Input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Enter your name..."
className="flex-1"
/>
<Button
onClick={() => refetch()}
disabled={isLoading}
className="min-w-[100px]"
>
{isLoading ? (
<InlineSpinner />
) : (
<>
<Sparkles className="h-4 w-4 mr-2" />
Send
</>
)}
</Button>
</div>
{data && <CodePreview data={data} />}
</DemoCard>
);
}

View File

@ -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 (
<DemoCard
title="Posts"
description="With publish action and filtering"
icon={FileText}
iconColorClass="bg-accent/10 text-accent"
glowClass="glow-accent"
>
{/* Create Form */}
<CreatePostForm
title={title}
content={content}
authorId={authorId}
onTitleChange={setTitle}
onContentChange={setContent}
onAuthorIdChange={setAuthorId}
onSubmit={handleCreate}
isLoading={createMutation.isPending}
/>
{/* Posts List */}
{isLoading ? (
<LoadingSpinner colorClass="text-accent" />
) : (
<div className="space-y-3">
{posts?.length === 0 && (
<EmptyState message="No posts yet. Create your first post!" />
)}
{posts?.map((post) => (
<PostCard
key={post.id}
post={post}
onPublish={() => publishMutation.mutate({ id: post.id })}
onDelete={() => deleteMutation.mutate({ id: post.id })}
/>
))}
</div>
)}
</DemoCard>
);
}
// ============================================================================
// 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 (
<div className="space-y-3">
<div className="flex gap-3">
<Input
value={title}
onChange={(e) => onTitleChange(e.target.value)}
placeholder="Post title..."
className="flex-1"
/>
<div className="w-28">
<Label className="text-xs text-muted-foreground mb-1 block">
Author ID
</Label>
<Input
value={authorId}
onChange={(e) => onAuthorIdChange(e.target.value)}
type="number"
/>
</div>
</div>
<div className="flex gap-3">
<Input
value={content}
onChange={(e) => onContentChange(e.target.value)}
placeholder="Write something amazing..."
className="flex-1"
/>
<Button onClick={onSubmit} disabled={!title || !content || isLoading}>
{isLoading ? (
<InlineSpinner />
) : (
<>
<Plus className="h-4 w-4 mr-2" />
Create
</>
)}
</Button>
</div>
</div>
);
}
interface PostCardProps {
post: Post;
onPublish: () => void;
onDelete: () => void;
}
function PostCard({ post, onPublish, onDelete }: PostCardProps) {
return (
<div className="item-row p-4 rounded-lg bg-muted/30 border border-border/50">
<div className="flex items-start gap-4">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<h3 className="font-semibold truncate">{post.title}</h3>
<StatusBadge status={post.published ? "success" : "warning"}>
{post.published ? "Published" : "Draft"}
</StatusBadge>
</div>
<p className="text-sm text-muted-foreground line-clamp-2">
{post.content}
</p>
</div>
<div className="flex items-center gap-1 shrink-0">
{!post.published && (
<Button
size="sm"
variant="outline"
className="text-secondary border-secondary/30 hover:bg-secondary/10"
onClick={onPublish}
>
<Sparkles className="h-3 w-3 mr-1" />
Publish
</Button>
)}
<Button
size="icon"
variant="ghost"
className="text-muted-foreground hover:text-destructive"
onClick={onDelete}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
</div>
);
}

View File

@ -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<number | null>(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 (
<DemoCard
title="Users"
description="Full CRUD operations example"
icon={Users}
iconColorClass="bg-secondary/10 text-secondary"
glowClass="glow-secondary"
>
{/* Create Form */}
<CreateUserForm
name={newName}
email={newEmail}
onNameChange={setNewName}
onEmailChange={setNewEmail}
onSubmit={handleCreate}
isLoading={createMutation.isPending}
/>
{/* Users List */}
{isLoading ? (
<LoadingSpinner colorClass="text-secondary" />
) : (
<div className="space-y-2">
{users?.length === 0 && (
<EmptyState message="No users yet. Create one above!" />
)}
{users?.map((user) => (
<UserRow
key={user.id}
user={user}
isEditing={editingId === user.id}
editName={editName}
editEmail={editEmail}
onEditNameChange={setEditName}
onEditEmailChange={setEditEmail}
onStartEdit={() => startEditing(user)}
onCancelEdit={() => setEditingId(null)}
onSaveEdit={() => handleUpdate(user.id)}
onDelete={() => deleteMutation.mutate({ id: user.id })}
/>
))}
</div>
)}
</DemoCard>
);
}
// ============================================================================
// 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 (
<div className="flex gap-3">
<Input
value={name}
onChange={(e) => onNameChange(e.target.value)}
placeholder="Name"
className="flex-1"
/>
<Input
value={email}
onChange={(e) => onEmailChange(e.target.value)}
placeholder="Email"
className="flex-1"
/>
<Button
onClick={onSubmit}
disabled={!name || !email || isLoading}
size="icon"
className="shrink-0"
>
{isLoading ? <InlineSpinner /> : <Plus className="h-4 w-4" />}
</Button>
</div>
);
}
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 (
<ItemRow>
<Input
value={editName}
onChange={(e) => onEditNameChange(e.target.value)}
className="flex-1"
/>
<Input
value={editEmail}
onChange={(e) => onEditEmailChange(e.target.value)}
className="flex-1"
/>
<Button
size="icon"
variant="ghost"
className="text-green-500 hover:text-green-600 hover:bg-green-500/10"
onClick={onSaveEdit}
>
<Check className="h-4 w-4" />
</Button>
<Button
size="icon"
variant="ghost"
className="text-muted-foreground hover:text-foreground"
onClick={onCancelEdit}
>
<X className="h-4 w-4" />
</Button>
</ItemRow>
);
}
return (
<ItemRow>
<Avatar name={user.name} />
<div className="flex-1">
<div className="font-medium">{user.name}</div>
<div className="text-sm text-muted-foreground">{user.email}</div>
</div>
<Button
size="icon"
variant="ghost"
className="text-muted-foreground hover:text-primary"
onClick={onStartEdit}
>
<Edit2 className="h-4 w-4" />
</Button>
<Button
size="icon"
variant="ghost"
className="text-muted-foreground hover:text-destructive"
onClick={onDelete}
>
<Trash2 className="h-4 w-4" />
</Button>
</ItemRow>
);
}

View File

@ -0,0 +1,4 @@
export { HelloExample } from "./HelloExample";
export { UsersExample } from "./UsersExample";
export { PostsExample } from "./PostsExample";
export { FileUploadExample } from "./FileUploadExample";

View File

@ -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";

View File

@ -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<HTMLDivElement> & VariantProps<typeof alertVariants>
>(({ className, variant, ...props }, ref) => (
<div
ref={ref}
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
))
Alert.displayName = "Alert"
const AlertTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h5
ref={ref}
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
{...props}
/>
))
AlertTitle.displayName = "AlertTitle"
const AlertDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm [&_p]:leading-relaxed", className)}
{...props}
/>
))
AlertDescription.displayName = "AlertDescription"
export { Alert, AlertTitle, AlertDescription }

View File

@ -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<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
}
);
Button.displayName = "Button";
export { Button, buttonVariants };

View File

@ -0,0 +1,79 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-lg border bg-card text-card-foreground shadow-sm",
className
)}
{...props}
/>
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"text-2xl font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

View File

@ -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<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean;
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRight className="ml-auto" />
</DropdownMenuPrimitive.SubTrigger>
));
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName;
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]",
className
)}
{...props}
/>
));
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName;
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
));
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
inset && "pl-8",
className
)}
{...props}
/>
));
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
));
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName;
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
));
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
)}
{...props}
/>
));
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
));
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
);
};
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
};

View File

@ -0,0 +1,22 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
export { Input }

View File

@ -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<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }

View File

@ -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<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
))
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-[--radix-select-content-available-height] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-select-content-transform-origin]",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
{...props}
/>
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
}

View File

@ -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<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
className
)}
{...props}
/>
))
TabsList.displayName = TabsPrimitive.List.displayName
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
className
)}
{...props}
/>
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className
)}
{...props}
/>
))
TabsContent.displayName = TabsPrimitive.Content.displayName
export { Tabs, TabsList, TabsTrigger, TabsContent }

View File

@ -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 (
<textarea
className={cn(
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
ref={ref}
{...props}
/>
)
})
Textarea.displayName = "Textarea"
export { Textarea }

465
apps/client/src/index.css Normal file
View File

@ -0,0 +1,465 @@
@import "tailwindcss";
@import "tw-animate-css";
@theme {
--color-background: hsl(var(--background));
--color-foreground: hsl(var(--foreground));
--color-card: hsl(var(--card));
--color-card-foreground: hsl(var(--card-foreground));
--color-popover: hsl(var(--popover));
--color-popover-foreground: hsl(var(--popover-foreground));
--color-primary: hsl(var(--primary));
--color-primary-foreground: hsl(var(--primary-foreground));
--color-secondary: hsl(var(--secondary));
--color-secondary-foreground: hsl(var(--secondary-foreground));
--color-muted: hsl(var(--muted));
--color-muted-foreground: hsl(var(--muted-foreground));
--color-accent: hsl(var(--accent));
--color-accent-foreground: hsl(var(--accent-foreground));
--color-destructive: hsl(var(--destructive));
--color-destructive-foreground: hsl(var(--destructive-foreground));
--color-border: hsl(var(--border));
--color-input: hsl(var(--input));
--color-ring: hsl(var(--ring));
--radius-lg: var(--radius);
--radius-md: calc(var(--radius) - 2px);
--radius-sm: calc(var(--radius) - 4px);
--animate-accordion-down: accordion-down 0.2s ease-out;
--animate-accordion-up: accordion-up 0.2s ease-out;
}
@layer base {
/* ========================================================================
THEME: Purple (Default) - Vibrant purple with teal & pink accents
======================================================================== */
:root,
.theme-purple {
--background: 270 50% 98%;
--foreground: 270 50% 10%;
--card: 0 0% 100%;
--card-foreground: 270 50% 10%;
--popover: 0 0% 100%;
--popover-foreground: 270 50% 10%;
--primary: 270 80% 60%;
--primary-foreground: 0 0% 100%;
--secondary: 175 60% 45%;
--secondary-foreground: 0 0% 100%;
--muted: 270 30% 94%;
--muted-foreground: 270 20% 45%;
--accent: 330 80% 65%;
--accent-foreground: 0 0% 100%;
--destructive: 0 75% 55%;
--destructive-foreground: 0 0% 100%;
--border: 270 30% 88%;
--input: 270 30% 88%;
--ring: 270 80% 60%;
--radius: 1rem;
--theme-gradient: linear-gradient(135deg, hsl(270 80% 60%) 0%, hsl(330 85% 60%) 50%, hsl(175 70% 50%) 100%);
}
.dark.theme-purple,
.dark:not([class*="theme-"]) {
--background: 270 40% 8%;
--foreground: 270 20% 95%;
--card: 270 40% 12%;
--card-foreground: 270 20% 95%;
--popover: 270 40% 12%;
--popover-foreground: 270 20% 95%;
--primary: 270 80% 65%;
--primary-foreground: 270 40% 8%;
--secondary: 175 70% 50%;
--secondary-foreground: 270 40% 8%;
--muted: 270 30% 18%;
--muted-foreground: 270 15% 65%;
--accent: 330 85% 60%;
--accent-foreground: 0 0% 100%;
--destructive: 0 70% 50%;
--destructive-foreground: 0 0% 100%;
--border: 270 30% 22%;
--input: 270 30% 22%;
--ring: 270 80% 65%;
}
/* ========================================================================
THEME: Ocean - Deep blues with cyan accents
======================================================================== */
.theme-ocean {
--background: 210 50% 98%;
--foreground: 210 50% 10%;
--card: 0 0% 100%;
--card-foreground: 210 50% 10%;
--popover: 0 0% 100%;
--popover-foreground: 210 50% 10%;
--primary: 210 90% 55%;
--primary-foreground: 0 0% 100%;
--secondary: 185 80% 45%;
--secondary-foreground: 0 0% 100%;
--muted: 210 30% 94%;
--muted-foreground: 210 20% 45%;
--accent: 195 85% 50%;
--accent-foreground: 0 0% 100%;
--destructive: 0 75% 55%;
--destructive-foreground: 0 0% 100%;
--border: 210 30% 88%;
--input: 210 30% 88%;
--ring: 210 90% 55%;
--theme-gradient: linear-gradient(135deg, hsl(210 90% 55%) 0%, hsl(185 80% 45%) 50%, hsl(195 85% 50%) 100%);
}
.dark.theme-ocean {
--background: 210 50% 6%;
--foreground: 210 20% 95%;
--card: 210 45% 10%;
--card-foreground: 210 20% 95%;
--popover: 210 45% 10%;
--popover-foreground: 210 20% 95%;
--primary: 210 85% 60%;
--primary-foreground: 210 50% 6%;
--secondary: 185 75% 50%;
--secondary-foreground: 210 50% 6%;
--muted: 210 35% 16%;
--muted-foreground: 210 15% 65%;
--accent: 195 80% 55%;
--accent-foreground: 0 0% 100%;
--destructive: 0 70% 50%;
--destructive-foreground: 0 0% 100%;
--border: 210 35% 20%;
--input: 210 35% 20%;
--ring: 210 85% 60%;
}
/* ========================================================================
THEME: Sunset - Warm oranges and reds
======================================================================== */
.theme-sunset {
--background: 30 50% 98%;
--foreground: 20 50% 10%;
--card: 0 0% 100%;
--card-foreground: 20 50% 10%;
--popover: 0 0% 100%;
--popover-foreground: 20 50% 10%;
--primary: 25 95% 55%;
--primary-foreground: 0 0% 100%;
--secondary: 350 80% 55%;
--secondary-foreground: 0 0% 100%;
--muted: 30 30% 94%;
--muted-foreground: 20 20% 45%;
--accent: 45 90% 55%;
--accent-foreground: 20 50% 10%;
--destructive: 0 75% 55%;
--destructive-foreground: 0 0% 100%;
--border: 30 30% 88%;
--input: 30 30% 88%;
--ring: 25 95% 55%;
--theme-gradient: linear-gradient(135deg, hsl(25 95% 55%) 0%, hsl(350 80% 55%) 50%, hsl(45 90% 55%) 100%);
}
.dark.theme-sunset {
--background: 20 40% 7%;
--foreground: 30 20% 95%;
--card: 20 40% 11%;
--card-foreground: 30 20% 95%;
--popover: 20 40% 11%;
--popover-foreground: 30 20% 95%;
--primary: 25 90% 58%;
--primary-foreground: 20 40% 7%;
--secondary: 350 75% 58%;
--secondary-foreground: 0 0% 100%;
--muted: 20 30% 16%;
--muted-foreground: 30 15% 65%;
--accent: 45 85% 58%;
--accent-foreground: 20 50% 10%;
--destructive: 0 70% 50%;
--destructive-foreground: 0 0% 100%;
--border: 20 30% 20%;
--input: 20 30% 20%;
--ring: 25 90% 58%;
}
/* ========================================================================
THEME: Forest - Earthy greens with warm accents
======================================================================== */
.theme-forest {
--background: 140 30% 97%;
--foreground: 140 40% 10%;
--card: 0 0% 100%;
--card-foreground: 140 40% 10%;
--popover: 0 0% 100%;
--popover-foreground: 140 40% 10%;
--primary: 145 70% 40%;
--primary-foreground: 0 0% 100%;
--secondary: 85 55% 45%;
--secondary-foreground: 0 0% 100%;
--muted: 140 20% 93%;
--muted-foreground: 140 15% 40%;
--accent: 35 80% 50%;
--accent-foreground: 0 0% 100%;
--destructive: 0 75% 55%;
--destructive-foreground: 0 0% 100%;
--border: 140 20% 87%;
--input: 140 20% 87%;
--ring: 145 70% 40%;
--theme-gradient: linear-gradient(135deg, hsl(145 70% 40%) 0%, hsl(85 55% 45%) 50%, hsl(35 80% 50%) 100%);
}
.dark.theme-forest {
--background: 140 35% 7%;
--foreground: 140 15% 95%;
--card: 140 35% 11%;
--card-foreground: 140 15% 95%;
--popover: 140 35% 11%;
--popover-foreground: 140 15% 95%;
--primary: 145 65% 45%;
--primary-foreground: 140 35% 7%;
--secondary: 85 50% 50%;
--secondary-foreground: 140 35% 7%;
--muted: 140 25% 16%;
--muted-foreground: 140 10% 65%;
--accent: 35 75% 55%;
--accent-foreground: 0 0% 100%;
--destructive: 0 70% 50%;
--destructive-foreground: 0 0% 100%;
--border: 140 25% 20%;
--input: 140 25% 20%;
--ring: 145 65% 45%;
}
/* ========================================================================
THEME: Rose - Soft pinks with elegant accents
======================================================================== */
.theme-rose {
--background: 350 50% 98%;
--foreground: 350 40% 10%;
--card: 0 0% 100%;
--card-foreground: 350 40% 10%;
--popover: 0 0% 100%;
--popover-foreground: 350 40% 10%;
--primary: 350 80% 60%;
--primary-foreground: 0 0% 100%;
--secondary: 320 70% 55%;
--secondary-foreground: 0 0% 100%;
--muted: 350 25% 94%;
--muted-foreground: 350 20% 45%;
--accent: 15 85% 60%;
--accent-foreground: 0 0% 100%;
--destructive: 0 75% 55%;
--destructive-foreground: 0 0% 100%;
--border: 350 25% 88%;
--input: 350 25% 88%;
--ring: 350 80% 60%;
--theme-gradient: linear-gradient(135deg, hsl(350 80% 60%) 0%, hsl(320 70% 55%) 50%, hsl(15 85% 60%) 100%);
}
.dark.theme-rose {
--background: 350 35% 7%;
--foreground: 350 15% 95%;
--card: 350 35% 11%;
--card-foreground: 350 15% 95%;
--popover: 350 35% 11%;
--popover-foreground: 350 15% 95%;
--primary: 350 75% 62%;
--primary-foreground: 350 35% 7%;
--secondary: 320 65% 58%;
--secondary-foreground: 0 0% 100%;
--muted: 350 25% 16%;
--muted-foreground: 350 12% 65%;
--accent: 15 80% 62%;
--accent-foreground: 0 0% 100%;
--destructive: 0 70% 50%;
--destructive-foreground: 0 0% 100%;
--border: 350 25% 20%;
--input: 350 25% 20%;
--ring: 350 75% 62%;
}
/* ========================================================================
THEME: Slate - Minimal and professional
======================================================================== */
.theme-slate {
--background: 0 0% 98%;
--foreground: 220 15% 15%;
--card: 0 0% 100%;
--card-foreground: 220 15% 15%;
--popover: 0 0% 100%;
--popover-foreground: 220 15% 15%;
--primary: 220 15% 25%;
--primary-foreground: 0 0% 100%;
--secondary: 220 10% 50%;
--secondary-foreground: 0 0% 100%;
--muted: 220 10% 94%;
--muted-foreground: 220 10% 45%;
--accent: 220 15% 35%;
--accent-foreground: 0 0% 100%;
--destructive: 0 75% 55%;
--destructive-foreground: 0 0% 100%;
--border: 220 10% 88%;
--input: 220 10% 88%;
--ring: 220 15% 25%;
--theme-gradient: linear-gradient(135deg, hsl(220 15% 25%) 0%, hsl(220 10% 50%) 50%, hsl(220 15% 35%) 100%);
}
.dark.theme-slate {
--background: 220 20% 6%;
--foreground: 220 10% 95%;
--card: 220 18% 10%;
--card-foreground: 220 10% 95%;
--popover: 220 18% 10%;
--popover-foreground: 220 10% 95%;
--primary: 220 12% 70%;
--primary-foreground: 220 20% 6%;
--secondary: 220 10% 55%;
--secondary-foreground: 0 0% 100%;
--muted: 220 15% 15%;
--muted-foreground: 220 8% 60%;
--accent: 220 12% 45%;
--accent-foreground: 0 0% 100%;
--destructive: 0 70% 50%;
--destructive-foreground: 0 0% 100%;
--border: 220 15% 18%;
--input: 220 15% 18%;
--ring: 220 12% 70%;
}
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
font-feature-settings: "rlig" 1, "calt" 1;
}
.container {
margin-left: auto;
margin-right: auto;
padding-left: 1.5rem;
padding-right: 1.5rem;
max-width: 1400px;
width: 100%;
}
}
/* Animated gradient background - uses theme gradient */
.gradient-bg {
background: linear-gradient(
135deg,
hsl(var(--primary) / 0.08) 0%,
hsl(var(--secondary) / 0.08) 50%,
hsl(var(--accent) / 0.08) 100%
);
animation: gradient-shift 8s ease infinite;
background-size: 200% 200%;
}
@keyframes gradient-shift {
0%, 100% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
}
/* Card hover effects */
.card-hover {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.card-hover:hover {
box-shadow: 0 10px 20px -12px hsl(var(--primary) / 0.25);
}
/* Glassmorphism card */
.glass-card {
background: hsl(var(--card) / 0.8);
backdrop-filter: blur(12px);
border: 1px solid hsl(var(--border) / 0.5);
}
/* Gradient text - uses theme colors */
.gradient-text {
background: linear-gradient(135deg, hsl(var(--primary)) 0%, hsl(var(--accent)) 50%, hsl(var(--secondary)) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* Glow effects - use theme primary */
.glow-primary {
box-shadow: 0 0 20px hsl(var(--primary) / 0.3), 0 0 40px hsl(var(--primary) / 0.1);
}
.glow-secondary {
box-shadow: 0 0 20px hsl(var(--secondary) / 0.3), 0 0 40px hsl(var(--secondary) / 0.1);
}
.glow-accent {
box-shadow: 0 0 20px hsl(var(--accent) / 0.3), 0 0 40px hsl(var(--accent) / 0.1);
}
/* Item row hover */
.item-row {
transition: all 0.2s ease;
}
.item-row:hover {
background: hsl(var(--muted) / 0.8);
}
/* Status badges */
.badge-success {
background: linear-gradient(135deg, hsl(160 80% 40% / 0.2) 0%, hsl(175 70% 50% / 0.2) 100%);
color: hsl(160 80% 40%);
border: 1px solid hsl(160 80% 40% / 0.3);
}
.dark .badge-success {
background: linear-gradient(135deg, hsl(160 70% 45% / 0.2) 0%, hsl(175 70% 50% / 0.2) 100%);
color: hsl(160 70% 65%);
}
.badge-warning {
background: linear-gradient(135deg, hsl(45 90% 50% / 0.2) 0%, hsl(35 90% 55% / 0.2) 100%);
color: hsl(35 90% 40%);
border: 1px solid hsl(45 90% 50% / 0.3);
}
.dark .badge-warning {
background: linear-gradient(135deg, hsl(45 90% 50% / 0.2) 0%, hsl(35 90% 55% / 0.2) 100%);
color: hsl(45 90% 65%);
}
/* Pulse animation for loader */
.pulse-glow {
animation: pulse-glow 2s ease-in-out infinite;
}
@keyframes pulse-glow {
0%, 100% { opacity: 1; filter: drop-shadow(0 0 8px hsl(var(--primary) / 0.5)); }
50% { opacity: 0.7; filter: drop-shadow(0 0 16px hsl(var(--primary) / 0.8)); }
}
/* Theme picker swatch */
.theme-swatch {
width: 1.25rem;
height: 1.25rem;
border-radius: 9999px;
border: 2px solid transparent;
transition: all 0.2s ease;
cursor: pointer;
}
.theme-swatch:hover {
transform: scale(1.1);
}
.theme-swatch.active {
border-color: hsl(var(--foreground));
box-shadow: 0 0 0 2px hsl(var(--background)), 0 0 0 4px hsl(var(--foreground) / 0.3);
}
@keyframes accordion-down {
from { height: 0; }
to { height: var(--radix-accordion-content-height); }
}
@keyframes accordion-up {
from { height: var(--radix-accordion-content-height); }
to { height: 0; }
}

View File

@ -0,0 +1,36 @@
import { createTRPCReact, type CreateTRPCReact } from "@trpc/react-query";
import { httpBatchLink, httpLink, splitLink, type TRPCClient } from "@trpc/client";
import type { AppRouter } from "@repo/server/router";
/**
* Create tRPC React hooks
*/
export const trpc: CreateTRPCReact<AppRouter, unknown> =
createTRPCReact<AppRouter>();
/**
* Get the base URL for tRPC requests
*/
const BASE_URL =
(typeof globalThis.process !== "undefined" &&
globalThis.process.env?.VITE_TRPC_URL) ||
import.meta.env.VITE_TRPC_URL ||
"http://localhost:3001/api";
const trpcUrl = `${BASE_URL}/trpc`;
/**
* Create tRPC client
*
* Uses splitLink so that FormData mutations (multipart/form-data) are sent
* through httpLink (no batching), while all other requests use httpBatchLink.
*/
export const trpcClient: TRPCClient<AppRouter> = trpc.createClient({
links: [
splitLink({
condition: (op) => op.input instanceof FormData,
true: httpLink({ url: trpcUrl }),
false: httpBatchLink({ url: trpcUrl }),
}),
],
});

View File

@ -0,0 +1,7 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

38
apps/client/src/main.tsx Normal file
View File

@ -0,0 +1,38 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import App from "./App";
import "./index.css";
import TRPCDemoPage from "./pages/TRPCDemoPage";
import NotFoundPage from "./pages/NotFoundPage";
import { trpc, trpcClient } from "./lib/trpc";
// Create a React Query client
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 1000, // 5 seconds
},
},
});
const router = createBrowserRouter([
{
path: "/",
element: <App />,
children: [{ index: true, element: <TRPCDemoPage /> }],
},
{ path: "*", element: <NotFoundPage /> },
]);
createRoot(document.getElementById("root")!).render(
<StrictMode>
<trpc.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
</QueryClientProvider>
</trpc.Provider>
</StrictMode>
);

View File

@ -0,0 +1,26 @@
import { Link } from "react-router-dom";
import { Button } from "@/components/ui/button";
const NotFoundPage = () => {
return (
<section className="space-y-4 text-center">
<div className="space-y-2">
<p className="text-sm uppercase tracking-wide text-muted-foreground">
404
</p>
<h1 className="text-2xl font-semibold">Page not found</h1>
<p className="text-muted-foreground">
The page you are looking for does not exist or has moved.
</p>
</div>
<div className="flex justify-center">
<Button asChild>
<Link to="/">Go back home</Link>
</Button>
</div>
</section>
);
};
export default NotFoundPage;

View File

@ -0,0 +1,43 @@
import { Sparkles } from "lucide-react";
import {
HelloExample,
UsersExample,
PostsExample,
FileUploadExample,
} from "@/components/demo/examples";
export default function TRPCDemoPage() {
return (
<div className="min-h-screen gradient-bg">
<div className="container mx-auto p-8 max-w-4xl">
{/* Hero Header */}
<header className="mb-12 text-center">
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-primary/10 text-primary text-sm font-medium mb-4">
<Sparkles className="h-4 w-4" />
Type-safe API Demo
</div>
<h1 className="text-5xl font-bold mb-4 gradient-text">
tRPC + TaylorDB
</h1>
<p className="text-lg text-muted-foreground max-w-md mx-auto">
Experience the power of end-to-end type safety with a beautiful,
modern interface
</p>
</header>
{/* Demo Examples */}
<main className="grid gap-8">
<HelloExample />
<UsersExample />
<PostsExample />
<FileUploadExample />
</main>
{/* Footer */}
<footer className="mt-12 text-center text-sm text-muted-foreground">
<p>Built with 💜 using tRPC, React Query &amp; TaylorDB</p>
</footer>
</div>
</div>
);
}

View File

@ -0,0 +1,33 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2022",
"useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"types": ["vite/client", "node"],
"skipLibCheck": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
},
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"],
"exclude": []
}

View File

@ -0,0 +1,9 @@
{
"extends": "./tsconfig.app.json",
"compilerOptions": {
"composite": true,
"outDir": "./dist"
},
"include": ["src", "vite.config.ts"],
"exclude": ["node_modules", "dist"]
}

View File

@ -0,0 +1,26 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2023",
"lib": ["ES2023"],
"module": "ESNext",
"types": ["node"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

View File

@ -0,0 +1,83 @@
import tailwindcss from "@tailwindcss/vite";
import react from "@vitejs/plugin-react";
import path from "node:path";
import { PassThrough } from "stream";
import { defineConfig, type ViteDevServer } from "vite";
// A simple connection manager for SSE
const clients = new Set<PassThrough>();
type ConsoleLevel = "log" | "warn" | "error" | "info";
function sendLog(message: { level: ConsoleLevel; message: string }) {
clients.forEach((client) =>
client.write(`data: ${JSON.stringify(message)}\n\n`)
);
}
// Intercept console messages
const originalConsole = {
log: console.log,
warn: console.warn,
error: console.error,
info: console.info,
};
Object.keys(originalConsole).forEach((level) => {
console[level as ConsoleLevel] = (
...args: Parameters<Console[ConsoleLevel]>
) => {
sendLog({ level: level as ConsoleLevel, message: args.join(" ") });
originalConsole[level as ConsoleLevel](...args);
};
});
export default defineConfig({
plugins: [
tailwindcss(),
react(),
{
name: "sse-plugin",
apply: "serve" as const,
configureServer(server: ViteDevServer) {
server.middlewares.use("/sse-logs", (req, res) => {
res.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
});
const stream = new PassThrough();
clients.add(stream);
sendLog({ level: "info", message: "Client connected to SSE logs" });
stream.pipe(res);
req.on("close", () => {
clients.delete(stream);
sendLog({
level: "info",
message: "Client disconnected from SSE logs",
});
});
});
},
},
],
resolve: {
alias: {
"@": path.resolve(__dirname, "src"),
},
},
server: {
allowedHosts: [".develop.taylordb.ai", "localhost", "127.0.0.1"],
host: true,
port: 5173,
hmr: {
host: "",
protocol: "wss",
clientPort: 443,
path: "/__vite_hmr",
},
},
});

3
apps/server/.env.example Normal file
View File

@ -0,0 +1,3 @@
TAYLORDB_BASE_ID=
TAYLORDB_BASE_URL=
TAYLORDB_API_KEY=

53
apps/server/index.ts Normal file
View File

@ -0,0 +1,53 @@
import express from "express";
import cors from "cors";
import cookieParser from "cookie-parser";
import * as trpcExpress from "@trpc/server/adapters/express";
import { appRouter } from "./router.js";
import { createContext } from "./trpc.js";
const app = express();
const PORT = process.env.PORT || 3001;
// Parse cookies
app.use(cookieParser());
// Enable CORS for frontend (adjust origin in production)
app.use(
cors({
origin: [
process.env.FRONTEND_URL || "http://localhost:5173",
"http://localhost:5174",
],
credentials: true,
})
);
// Health check endpoint
app.get("/api/health", (_req, res) => {
res.json({ status: "ok", timestamp: new Date().toISOString() });
});
// tRPC middleware
app.use(
"/api/trpc",
(req, res, next) => {
// Handle root path access with a friendly message instead of a tRPC error
if (req.path === "/" || req.path === "") {
return res.json({
message: "TaylorDB tRPC server is running!",
health: `http://${req.headers.host}/api/trpc/health`,
timestamp: new Date().toISOString(),
});
}
next();
},
trpcExpress.createExpressMiddleware({
router: appRouter,
createContext,
})
);
// Start server
app.listen(PORT, () => {
console.log(`🚀 Server running on http://localhost:${PORT}`);
console.log(`📡 tRPC endpoint: http://localhost:${PORT}/api/trpc`);
});

36
apps/server/package.json Normal file
View File

@ -0,0 +1,36 @@
{
"name": "@repo/server",
"version": "1.0.0",
"type": "module",
"private": true,
"description": "tRPC backend server with Express",
"main": "dist/index.js",
"exports": {
"./router": "./router.ts"
},
"scripts": {
"dev": "tsx watch index.ts",
"build": "pnpm exec esbuild index.ts --bundle --outfile=dist/index.js --format=esm --platform=node --packages=external",
"start": "node dist/index.js"
},
"dependencies": {
"@taylordb/query-builder": "^0.11.4",
"@trpc/server": "^11.8.1",
"cookie-parser": "^1.4.7",
"cors": "^2.8.5",
"express": "^5.2.1",
"multer": "^2.0.2",
"zod": "^4.3.5"
},
"devDependencies": {
"@types/cookie-parser": "^1.4.10",
"@types/cors": "^2.8.19",
"@types/express": "^5.0.6",
"@types/multer": "^2.0.0",
"@types/node": "^24.10.1",
"concurrently": "^9.2.1",
"esbuild": "^0.27.2",
"tsx": "^4.21.0",
"typescript": "~5.9.3"
}
}

78
apps/server/router.ts Normal file
View File

@ -0,0 +1,78 @@
import { z } from "zod";
import { router, publicProcedure } from "./trpc";
import { usersRouter, postsRouter, submitUserDataRouter } from "./routers";
/**
* Main tRPC Router
*
* This router merges all sub-routers from the routers/ directory.
* Each domain (users, posts, etc.) has its own file for better organization.
*
* To add a new domain:
* 1. Create a new file in routers/ (e.g., routers/comments.ts)
* 2. Export the router from routers/index.ts
* 3. Import and add it below
*/
export const appRouter = router({
// ============================================================================
// Sub-Routers (organized by domain)
// ============================================================================
users: usersRouter,
posts: postsRouter,
submitUserData: submitUserDataRouter,
// ============================================================================
// Global / Utility Procedures
// ============================================================================
hello: publicProcedure
.input(
z
.object({
name: z.string().optional(),
})
.optional()
)
.query(({ input }) => {
return {
message: `Hello ${input?.name ?? "World"}!`,
timestamp: new Date().toISOString(),
uptime: process.uptime(),
};
}),
});
// Export type definition of API
export type AppRouter = typeof appRouter;
/**
* ============================================================================
* tRPC Quick Reference
* ============================================================================
*
* QUERIES (for reading data):
* - Use .query() for operations that don't modify data
* - Example: getAll, getById, search, etc.
*
* MUTATIONS (for writing data):
* - Use .mutation() for operations that create, update, or delete data
* - Example: create, update, delete
*
* INPUT VALIDATION:
* - Use .input() with Zod schemas to validate input
* - Example: .input(z.object({ id: z.number(), name: z.string() }))
*
* ACCESSING INPUT:
* - Access validated input via { input } destructuring
* - Example: .query(async ({ input }) => { ... })
*
* ORGANIZATION:
* - Group related procedures under a namespace
* - Example: users.getAll, users.create, posts.getAll, etc.
*
* ERROR HANDLING:
* - Throw errors from procedures, tRPC will handle them
* - Example: throw new Error("User not found");
*
* For comprehensive examples, see the example implementation or docs.
*/

View File

@ -0,0 +1,10 @@
/**
* Routers Index
*
* Re-export all sub-routers from a single entry point.
* This keeps imports clean in the main router.ts
*/
export { usersRouter } from "./users";
export { postsRouter } from "./posts";
export { submitUserDataRouter } from "./submitUserData";

View File

@ -0,0 +1,156 @@
import { z } from "zod";
import { router, publicProcedure } from "../trpc";
/**
* Posts Router
*
* Another example sub-router showing a different domain.
* Demonstrates relationships (author references users).
*
* Example using TaylorDB queryBuilder (available via ctx.queryBuilder):
*
* // Query all posts
* const posts = await ctx.queryBuilder.from("posts").select("*");
*
* // Query with filters
* const publishedPosts = await ctx.queryBuilder
* .from("posts")
* .where({ published: true })
* .select("*");
*
* // Create a new post
* const newPost = await ctx.queryBuilder
* .from("posts")
* .insert({
* title: "My Title",
* content: "My Content",
* authorId: 1,
* published: false
* });
*
* // Update a post
* const updatedPost = await ctx.queryBuilder
* .from("posts")
* .where({ id: 1 })
* .update({ published: true });
*
* // Delete a post
* await ctx.queryBuilder
* .from("posts")
* .where({ id: 1 })
* .delete();
*/
// In-memory store for demonstration
const posts: {
id: number;
title: string;
content: string;
authorId: number;
published: boolean;
createdAt: Date;
}[] = [
{
id: 1,
title: "Hello World",
content: "This is my first post!",
authorId: 1,
published: true,
createdAt: new Date(),
},
];
let nextId = 2;
export const postsRouter = router({
getAll: publicProcedure
.input(
z
.object({
published: z.boolean().optional(),
authorId: z.number().optional(),
})
.optional(),
)
.query(({ input }) => {
let result = posts;
if (input?.published !== undefined) {
result = result.filter((p) => p.published === input.published);
}
if (input?.authorId !== undefined) {
result = result.filter((p) => p.authorId === input.authorId);
}
return result;
}),
getById: publicProcedure
.input(z.object({ id: z.number() }))
.query(({ input }) => {
const post = posts.find((p) => p.id === input.id);
if (!post) throw new Error("Post not found");
return post;
}),
create: publicProcedure
.input(
z.object({
title: z.string().min(1).max(200),
content: z.string().min(1),
authorId: z.number(),
published: z.boolean().default(false),
})
)
.mutation(({ input }) => {
const newPost = {
id: nextId++,
title: input.title,
content: input.content,
authorId: input.authorId,
published: input.published,
createdAt: new Date(),
};
posts.push(newPost);
return newPost;
}),
update: publicProcedure
.input(
z.object({
id: z.number(),
title: z.string().min(1).max(200).optional(),
content: z.string().min(1).optional(),
published: z.boolean().optional(),
}),
)
.mutation(({ input }) => {
const post = posts.find((p) => p.id === input.id);
if (!post) throw new Error("Post not found");
if (input.title) post.title = input.title;
if (input.content) post.content = input.content;
if (input.published !== undefined) post.published = input.published;
return post;
}),
delete: publicProcedure
.input(z.object({ id: z.number() }))
.mutation(({ input }) => {
const index = posts.findIndex((p) => p.id === input.id);
if (index === -1) throw new Error("Post not found");
const [deleted] = posts.splice(index, 1);
return deleted;
}),
// Example of a more specific procedure
publish: publicProcedure
.input(z.object({ id: z.number() }))
.mutation(({ input }) => {
const post = posts.find((p) => p.id === input.id);
if (!post) throw new Error("Post not found");
post.published = true;
return post;
}),
});

View File

@ -0,0 +1,66 @@
import { z } from "zod";
import { router, publicProcedure } from "../trpc";
/**
* Submit User Data Router
*
* tRPC 11 router demonstrating multipart/form-data input support.
* Accepts name, email, cv, and profileImage in a single tRPC mutation.
*
* TaylorDB attachment pattern:
* const cv = input.get("cv") as File | null;
* const profileImage = input.get("profileImage") as File | null;
* const attachments = await ctx.queryBuilder.uploadAttachments([
* { file: cv, name: cv.name },
* { file: profileImage, name: profileImage.name },
* ]);
* await ctx.queryBuilder.insertInto("users").values({
* name,
* email,
* cv: attachments[0],
* profileImage: attachments[1],
* }).execute();
*/
export const submitUserDataRouter = router({
/** Submit user data with named file fields via multipart/form-data (tRPC 11) */
submit: publicProcedure
.input(z.instanceof(FormData))
.mutation(async ({ input }) => {
const name = input.get("name") as string | null;
const email = input.get("email") as string | null;
const cv = input.get("cv") as File | null;
const profileImage = input.get("profileImage") as File | null;
const fileInfo = (file: File | null) =>
file && file.size > 0
? { name: file.name, size: file.size, type: file.type }
: null;
// ──────────────────────────────────────────────────────────────────────
// TaylorDB Attachment Example
//
// File objects are available directly — pass them to uploadAttachments:
//
// const attachments = await ctx.queryBuilder.uploadAttachments([
// ...(cv ? [{ file: cv, name: cv.name }] : []),
// ...(profileImage ? [{ file: profileImage, name: profileImage.name }] : []),
// ]);
// await ctx.queryBuilder.insertInto("users").values({
// name,
// email,
// cv: attachments[0],
// profileImage: attachments[1],
// }).execute();
// ──────────────────────────────────────────────────────────────────────
return {
received: {
name,
email,
cv: fileInfo(cv),
profileImage: fileInfo(profileImage),
},
};
}),
});

View File

@ -0,0 +1,75 @@
import { z } from "zod";
import { router, publicProcedure } from "../trpc";
/**
* Users Router
*
* Example sub-router demonstrating CRUD operations.
* Replace with your actual taylordb implementation.
*/
// In-memory store for demonstration
const users: { id: number; name: string; email: string; createdAt: Date }[] = [
{ id: 1, name: "Alice", email: "alice@example.com", createdAt: new Date() },
{ id: 2, name: "Bob", email: "bob@example.com", createdAt: new Date() },
];
let nextId = 3;
export const usersRouter = router({
getAll: publicProcedure.query(() => {
return users;
}),
getById: publicProcedure
.input(z.object({ id: z.number() }))
.query(({ input }) => {
const user = users.find((u) => u.id === input.id);
if (!user) throw new Error("User not found");
return user;
}),
create: publicProcedure
.input(
z.object({
name: z.string().min(1),
email: z.string().email(),
})
)
.mutation(({ input }) => {
const newUser = {
id: nextId++,
name: input.name,
email: input.email,
createdAt: new Date(),
};
users.push(newUser);
return newUser;
}),
update: publicProcedure
.input(
z.object({
id: z.number(),
name: z.string().min(1).optional(),
email: z.string().email().optional(),
})
)
.mutation(({ input }) => {
const user = users.find((u) => u.id === input.id);
if (!user) throw new Error("User not found");
if (input.name) user.name = input.name;
if (input.email) user.email = input.email;
return user;
}),
delete: publicProcedure
.input(z.object({ id: z.number() }))
.mutation(({ input }) => {
const index = users.findIndex((u) => u.id === input.id);
if (index === -1) throw new Error("User not found");
const [deleted] = users.splice(index, 1);
return deleted;
}),
});

View File

@ -0,0 +1,465 @@
/**
* Copyright (c) 2025 TaylorDB
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
interface FileInformation {
fieldname: string;
originalname: string;
encoding: string;
mimetype: string;
destination: string;
filename: string;
path: string;
size: number;
format: string;
width: number;
height: number;
}
interface UploadResponse {
collectionName: string;
fileInformation: FileInformation;
metadata: {
thumbnails: any[];
clips: any[];
};
baseId: string;
storageAdaptor: string;
_id: string;
__v: number;
}
export interface AttachmentColumnValue {
url: string;
fileType: string;
size: number;
}
export class Attachment {
public readonly collectionName: string;
public readonly fileInformation: FileInformation;
public readonly metadata: { thumbnails: any[]; clips: any[] };
public readonly baseId: string;
public readonly storageAdaptor: string;
public readonly _id: string;
constructor(data: UploadResponse) {
this.collectionName = data.collectionName;
this.fileInformation = data.fileInformation;
this.metadata = data.metadata;
this.baseId = data.baseId;
this.storageAdaptor = data.storageAdaptor;
this._id = data._id;
}
toColumnValue(): AttachmentColumnValue {
return {
url: this.fileInformation.path,
fileType: this.fileInformation.mimetype,
size: this.fileInformation.size,
};
}
}
type IsWithinOperatorValue =
| 'pastWeek'
| 'pastMonth'
| 'pastYear'
| 'nextWeek'
| 'nextMonth'
| 'nextYear'
| 'daysFromNow'
| 'daysAgo'
| 'currentWeek'
| 'currentMonth'
| 'currentYear';
type DefaultDateFilterValue =
| (
| 'today'
| 'tomorrow'
| 'yesterday'
| 'oneWeekAgo'
| 'oneWeekFromNow'
| 'oneMonthAgo'
| 'oneMonthFromNow'
)
| ['exactDay' | 'exactTimestamp', string]
| ['daysAgo' | 'daysFromNow', number];
type DateFilters = {
'=': DefaultDateFilterValue;
'!=': DefaultDateFilterValue;
'<': DefaultDateFilterValue;
'>': DefaultDateFilterValue;
'<=': DefaultDateFilterValue;
'>=': DefaultDateFilterValue;
isWithIn:
| IsWithinOperatorValue
| { value: 'daysAgo' | 'daysFromNow'; date: number };
isEmpty: boolean;
isNotEmpty: boolean;
};
type DateAggregations = {
empty: number;
filled: number;
unique: number;
percentEmpty: number;
percentFilled: number;
percentUnique: number;
min: number | null;
max: number | null;
daysRange: number | null;
monthRange: number | null;
};
type TextFilters = {
'=': string;
'!=': string;
caseEqual: string;
hasAnyOf: string[];
contains: string;
startsWith: string;
endsWith: string;
doesNotContain: string;
isEmpty: never;
isNotEmpty: never;
};
type LinkFilters = {
hasAnyOf: number[];
hasAllOf: number[];
isExactly: number[];
'=': number;
hasNoneOf: number[];
contains: string;
doesNotContain: string;
isEmpty: never;
isNotEmpty: never;
};
type SelectFilters<O extends readonly string[]> = {
hasAnyOf: O[number][];
hasAllOf: O[number][];
isExactly: O[number][];
'=': O[number];
hasNoneOf: O[number][];
contains: string;
doesNotContain: string;
isEmpty: never;
isNotEmpty: never;
};
type LinkAggregations = {
empty: number;
filled: number;
percentEmpty: number;
percentFilled: number;
};
type NumberFilters = {
'=': number;
'!=': number;
'>': number;
'>=': number;
'<': number;
'<=': number;
hasAnyOf: number[];
hasNoneOf: number[];
isEmpty: never;
isNotEmpty: never;
};
type NumberAggregations = {
sum: number;
average: number;
median: number;
min: number | null;
max: number | null;
range: number;
standardDeviation: number;
histogram: Record<string, number>;
empty: number;
filled: number;
unique: number;
percentEmpty: number;
percentFilled: number;
percentUnique: number;
};
type CheckboxFilters = {
'=': number;
};
/**
*
* Column types
*
*/
export type ColumnType<
S,
U,
I,
R extends boolean,
F extends { [key: string]: any } = object,
A extends { [key: string]: any } = object,
> = {
raw: S;
insert: I;
update: U;
filters: F;
aggregations: A;
isRequired: R;
};
export type DateColumnType<R extends boolean> = ColumnType<
string,
string,
string,
R,
DateFilters,
DateAggregations
>;
export type TextColumnType<R extends boolean> = ColumnType<
string,
string,
string,
R,
TextFilters
>;
export type ALinkColumnType<
T extends string,
S,
U,
I,
R extends boolean,
F extends { [key: string]: any } = LinkFilters,
A extends LinkAggregations = LinkAggregations,
> = ColumnType<S, U, I, R, F, A> & {
linkedTo: T;
};
export type LinkColumnType<
T extends string,
R extends boolean,
> = ALinkColumnType<
T,
object,
number[] | { newIds: number[]; deletedIds: number[] },
number[],
R
>;
export type AttachmentColumnType<R extends boolean> = ALinkColumnType<
'attachmentTable',
Attachment[],
Attachment[] | { newIds: number[]; deletedIds: number[] } | number[],
Attachment[] | number[],
R
>;
export type SingleSelectColumnType<
O extends readonly string[],
R extends boolean,
> = ColumnType<O[number][], O[number][], O[number][], R, SelectFilters<O>>;
export type NumberColumnType<R extends boolean> = ColumnType<
number,
number,
number,
R,
NumberFilters,
NumberAggregations
>;
export type CheckboxColumnType<R extends boolean> = ColumnType<
boolean,
boolean,
boolean,
R,
CheckboxFilters
>;
export type AutoGeneratedNumberColumnType = ColumnType<
number,
never,
never,
false,
NumberFilters,
NumberAggregations
>;
export type AutoGeneratedDateColumnType = ColumnType<
string,
never,
never,
false,
DateFilters,
DateAggregations
>;
export type TableRaws<T extends keyof TaylorDatabase> = {
[K in keyof TaylorDatabase[T]]: TaylorDatabase[T][K] extends ColumnType<
infer S,
any,
any,
infer R,
any,
any
>
? R extends true
? S
: S | undefined
: never;
};
export type TableInserts<T extends keyof TaylorDatabase> = {
[K in keyof TaylorDatabase[T]]: TaylorDatabase[T][K] extends ColumnType<
any,
infer I,
any,
infer R,
any,
any
>
? R extends true
? I
: I | undefined
: never;
};
export type TableUpdates<T extends keyof TaylorDatabase> = {
[K in keyof TaylorDatabase[T]]: TaylorDatabase[T][K] extends ColumnType<
any,
any,
infer U,
any,
any,
any
>
? U
: never;
};
export type SelectTable = {
id: AutoGeneratedNumberColumnType;
name: TextColumnType<true>;
color: TextColumnType<true>;
};
export type AttachmentTable = {
id: AutoGeneratedNumberColumnType;
name: TextColumnType<true>;
metadata: TextColumnType<true>;
size: NumberColumnType<true>;
fileType: TextColumnType<true>;
url: TextColumnType<true>;
};
export type CollaboratorsTable = {
id: AutoGeneratedNumberColumnType;
name: TextColumnType<true>;
emailAddress: TextColumnType<true>;
avatar: TextColumnType<true>;
};
export type TaylorDatabase = {
/**
*
*
* Internal tables, these tables can not be queried directly.
*
*/
selectTable: SelectTable;
attachmentTable: AttachmentTable;
collaboratorsTable: CollaboratorsTable;
calories: CaloriesTable;
strength: StrengthTable;
cardio: CardioTable;
weight: WeightTable;
goals: GoalsTable;
settings: SettingsTable;
};
export const CaloriesTimeOfDayOptions = ['Breakfast', 'Lunch', 'Dinner', 'Supper', 'Snack'] as const;
export const CaloriesUnitOptions = ['1 tsp = 5 mL ≈ 0.17 fl oz', '1 tbsp = 15 mL ≈ 0.5 fl oz', '1 cup = 240 mL = 8 fl oz', '1 fl oz ≈ 29.57 mL', '1 mL ≈ 0.034 fl oz', '1 L ≈ 33.814 fl oz', '1 g ≈ 0.035 oz', '1 oz ≈ 28.35 g', '1 kg ≈ 2.205 lb', '1 lb ≈ 453.59 g'] as const;
type CaloriesTable = {
id: NumberColumnType<false>;
createdAt: AutoGeneratedDateColumnType;
updatedAt: AutoGeneratedDateColumnType;
date: DateColumnType<false>;
timeOfDay: SingleSelectColumnType<typeof CaloriesTimeOfDayOptions, false>;
proteinPer100G: NumberColumnType<false>;
carbsPer100G: NumberColumnType<false>;
fatsPer100G: NumberColumnType<false>;
totalCalories: NumberColumnType<false>;
totalCarbs: NumberColumnType<false>;
totalFats: NumberColumnType<false>;
totalProtein: NumberColumnType<false>;
mealName: TextColumnType<false>;
name: TextColumnType<false>;
mealIngredient: TextColumnType<false>;
quantity: NumberColumnType<false>;
unit: SingleSelectColumnType<typeof CaloriesUnitOptions, false>;
quantityInGramsmL: NumberColumnType<false>;
quantityInFlOzozlb: NumberColumnType<false>;
};
export const StrengthExerciseOptions = ['Push-ups', 'Pull-ups', 'Pistol Squat', 'Deadlifts', 'Bench Press', 'Sit-ups', 'Lunges', 'Squats', 'Cable Pull-downs', 'Diamond Push-ups', 'Biceps Curls'] as const;
type StrengthTable = {
id: NumberColumnType<false>;
createdAt: AutoGeneratedDateColumnType;
updatedAt: AutoGeneratedDateColumnType;
reps: NumberColumnType<false>;
weight: NumberColumnType<false>;
date: DateColumnType<false>;
name: TextColumnType<false>;
exercise: SingleSelectColumnType<typeof StrengthExerciseOptions, false>;
};
export const CardioExerciseOptions = ['Running', 'Cycling', 'Swimming'] as const;
type CardioTable = {
id: NumberColumnType<false>;
createdAt: AutoGeneratedDateColumnType;
updatedAt: AutoGeneratedDateColumnType;
date: DateColumnType<false>;
duration: NumberColumnType<false>;
distance: NumberColumnType<false>;
exercise: SingleSelectColumnType<typeof CardioExerciseOptions, false>;
name: TextColumnType<false>;
speed: NumberColumnType<false>;
};
type WeightTable = {
id: NumberColumnType<false>;
createdAt: AutoGeneratedDateColumnType;
updatedAt: AutoGeneratedDateColumnType;
date: DateColumnType<false>;
weight: NumberColumnType<false>;
name: TextColumnType<false>;
};
type GoalsTable = {
id: NumberColumnType<false>;
createdAt: AutoGeneratedDateColumnType;
updatedAt: AutoGeneratedDateColumnType;
name: TextColumnType<false>;
value: TextColumnType<false>;
description: TextColumnType<false>;
};
type SettingsTable = {
id: NumberColumnType<false>;
createdAt: AutoGeneratedDateColumnType;
updatedAt: AutoGeneratedDateColumnType;
name: TextColumnType<false>;
value: TextColumnType<false>;
description: TextColumnType<false>;
};

42
apps/server/trpc.ts Normal file
View File

@ -0,0 +1,42 @@
import { createQueryBuilder } from "@taylordb/query-builder";
import { initTRPC } from "@trpc/server";
import type { CreateExpressContextOptions } from "@trpc/server/adapters/express";
import type { TaylorDatabase } from "./taylordb/types";
/**
* Create context for each tRPC request
* This is where you can add user session, database clients, etc.
*/
export const createContext = ({ req, res }: CreateExpressContextOptions) => {
// Extract app_access_token from cookies
const appAccessToken = req.cookies?.app_access_token;
if (!appAccessToken) {
throw new Error("Unauthorized: app_access_token cookie is required");
}
const queryBuilder = createQueryBuilder<TaylorDatabase>({
baseUrl: process.env.TAYLORDB_BASE_URL!,
baseId: process.env.TAYLORDB_SERVER_ID!,
apiKey: appAccessToken,
});
return {
req,
res,
queryBuilder,
};
};
export type Context = Awaited<ReturnType<typeof createContext>>;
/**
* Initialize tRPC instance
*/
const t = initTRPC.context<Context>().create();
/**
* Export reusable router and procedure helpers
*/
export const router = t.router;
export const publicProcedure = t.procedure;

20
apps/server/tsconfig.json Normal file
View File

@ -0,0 +1,20 @@
{
"compilerOptions": {
"composite": false,
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["ES2020"],
"outDir": "./dist",
"rootDir": ".",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true,
"types": ["node"]
},
"include": ["./**/*"],
"exclude": ["node_modules", "dist"]
}

View File

@ -0,0 +1,55 @@
# shadcn/ui Dashboard Components Guide
This is the **entry point** for shadcn/ui documentation in this template. The guide is split into smaller files so agents (and humans) can jump to the topic they need.
---
## 📚 Topics
- **Installation**
See `SHADCN_INSTALLATION.md` for:
- Installing individual components (core, dashboard, data display, advanced)
- Command examples for `pnpm dlx shadcn@latest add`
- **Dashboard Patterns**
See `SHADCN_DASHBOARD_PATTERNS.md` for:
- Stats cards layout
- Data table with dropdown actions
- Create/edit form in a dialog
- Loading states with Skeleton
- Tabs for different views
- Status badges
- Toast notifications (with tRPC mutations)
- Form with validation (react-hook-form + zod)
- Side sheet for details
- Command palette (search, Cmd/Ctrl+K)
- **Design & Layout**
See `SHADCN_DESIGN_AND_LAYOUT.md` for:
- Color schemes (semantic tokens)
- Spacing conventions
- Icons (lucide-react)
- Responsive grids and hide-on-mobile
- Performance tips (lazy dialogs, virtualization, skeletons, optimistic updates, debounce)
---
## How Agents Should Use These Docs
1. **Need to add a component?**
Use `SHADCN_INSTALLATION.md` for the exact `pnpm dlx shadcn@latest add` commands.
2. **Building a dashboard or CRUD UI?**
Use `SHADCN_DASHBOARD_PATTERNS.md` for copy-paste patterns (tables, dialogs, forms, toasts, sheets, etc.).
3. **Styling and layout?**
Use `SHADCN_DESIGN_AND_LAYOUT.md` for tokens, spacing, responsive patterns, and performance.
---
## Resources
- **shadcn/ui**: https://ui.shadcn.com/
- **Tailwind CSS**: https://tailwindcss.com/
- **Lucide Icons**: https://lucide.dev/
- **React Hook Form**: https://react-hook-form.com/

View File

@ -0,0 +1,566 @@
# shadcn/ui — Dashboard Patterns
Examples of using shadcn/ui components to build dashboard interfaces for TaylorDB applications.
---
## 1. Dashboard Layout with Stats Cards
```typescript
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
export default function DashboardPage() {
return (
<div className="container mx-auto p-8">
<h1 className="text-3xl font-bold mb-6">Dashboard</h1>
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-4">
<Card>
<CardHeader>
<CardTitle className="text-sm font-medium text-muted-foreground">
Total Users
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold">1,234</div>
<p className="text-xs text-muted-foreground mt-1">
+20% from last month
</p>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-sm font-medium text-muted-foreground">
Revenue
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold">$45,231</div>
<p className="text-xs text-muted-foreground mt-1">
+12% from last month
</p>
</CardContent>
</Card>
{/* More stats cards... */}
</div>
</div>
);
}
```
---
## 2. Data Table with Actions
```typescript
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { MoreHorizontal, Pencil, Trash } from "lucide-react";
export default function UsersTable({ users }: { users: User[] }) {
return (
<Card>
<CardHeader>
<CardTitle>Users</CardTitle>
<CardDescription>Manage your user accounts</CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Email</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{users.map((user) => (
<TableRow key={user.id}>
<TableCell className="font-medium">{user.name}</TableCell>
<TableCell>{user.email}</TableCell>
<TableCell>
<Badge
variant={user.status === "active" ? "default" : "secondary"}
>
{user.status}
</Badge>
</TableCell>
<TableCell className="text-right">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem>
<Pencil className="mr-2 h-4 w-4" />
Edit
</DropdownMenuItem>
<DropdownMenuItem className="text-destructive">
<Trash className="mr-2 h-4 w-4" />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
);
}
```
---
## 3. Create/Edit Form in Dialog
```typescript
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useState } from "react";
import { PlusIcon } from "lucide-react";
export default function CreateUserDialog() {
const [open, setOpen] = useState(false);
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const createMutation = trpc.users.create.useMutation({
onSuccess: () => {
setOpen(false);
setName("");
setEmail("");
},
});
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button>
<PlusIcon className="mr-2 h-4 w-4" />
Add User
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Create New User</DialogTitle>
<DialogDescription>Add a new user to your database</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="name">Name</Label>
<Input
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="John Doe"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="john@example.com"
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setOpen(false)}>
Cancel
</Button>
<Button
onClick={() => createMutation.mutate({ name, email })}
disabled={createMutation.isPending}
>
Create User
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
```
---
## 4. Loading States with Skeleton
```typescript
import { Skeleton } from "@/components/ui/skeleton";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
export default function DashboardSkeleton() {
return (
<div className="container mx-auto p-8">
<Skeleton className="h-10 w-48 mb-6" />
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-4">
{[...Array(4)].map((_, i) => (
<Card key={i}>
<CardHeader>
<Skeleton className="h-4 w-24" />
</CardHeader>
<CardContent>
<Skeleton className="h-8 w-20 mb-2" />
<Skeleton className="h-3 w-32" />
</CardContent>
</Card>
))}
</div>
</div>
);
}
// Usage in main component
export default function DashboardPage() {
const { data: stats, isLoading } = trpc.dashboard.getStats.useQuery();
if (isLoading) {
return <DashboardSkeleton />;
}
return <div>{/* Actual dashboard */}</div>;
}
```
---
## 5. Tabs for Different Views
```typescript
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
export default function DataExplorer() {
return (
<Card>
<CardHeader>
<CardTitle>Data Explorer</CardTitle>
</CardHeader>
<CardContent>
<Tabs defaultValue="overview">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="analytics">Analytics</TabsTrigger>
<TabsTrigger value="reports">Reports</TabsTrigger>
</TabsList>
<TabsContent value="overview" className="space-y-4">
{/* Overview content */}
</TabsContent>
<TabsContent value="analytics" className="space-y-4">
{/* Analytics content */}
</TabsContent>
<TabsContent value="reports" className="space-y-4">
{/* Reports content */}
</TabsContent>
</Tabs>
</CardContent>
</Card>
);
}
```
---
## 6. Status Badges
```typescript
import { Badge } from "@/components/ui/badge";
function StatusBadge({ status }: { status: string }) {
const variants = {
active: "default",
pending: "secondary",
inactive: "outline",
error: "destructive",
} as const;
return (
<Badge variant={variants[status as keyof typeof variants] || "outline"}>
{status}
</Badge>
);
}
// Usage
<StatusBadge status="active" />;
```
---
## 7. Toast Notifications
```typescript
import { useToast } from "@/hooks/use-toast";
import { Button } from "@/components/ui/button";
export default function ActionsPage() {
const { toast } = useToast();
const deleteMutation = trpc.items.delete.useMutation({
onSuccess: () => {
toast({
title: "Success!",
description: "Item deleted successfully",
});
},
onError: (error) => {
toast({
title: "Error",
description: error.message,
variant: "destructive",
});
},
});
return (
<Button onClick={() => deleteMutation.mutate({ id: 1 })}>
Delete Item
</Button>
);
}
// Don't forget to add <Toaster /> in your App.tsx or layout
```
---
## 8. Form with Validation
```typescript
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
const formSchema = z.object({
name: z.string().min(2, "Name must be at least 2 characters"),
email: z.string().email("Invalid email address"),
age: z.number().min(0).max(120),
});
export default function UserForm() {
const form = useForm({
resolver: zodResolver(formSchema),
defaultValues: {
name: "",
email: "",
age: 0,
},
});
const createMutation = trpc.users.create.useMutation({
onSuccess: () => {
form.reset();
},
});
const onSubmit = (data: z.infer<typeof formSchema>) => {
createMutation.mutate(data);
};
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder="John Doe" {...field} />
</FormControl>
<FormDescription>
Your full name as it appears on documents.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input type="email" placeholder="john@example.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" disabled={createMutation.isPending}>
{createMutation.isPending ? "Submitting..." : "Submit"}
</Button>
</form>
</Form>
);
}
```
---
## 9. Side Sheet for Details
```typescript
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet";
import { Button } from "@/components/ui/button";
export default function UserDetailsSheet({ userId }: { userId: number }) {
const { data: user } = trpc.users.getById.useQuery({ id: userId });
return (
<Sheet>
<SheetTrigger asChild>
<Button variant="outline">View Details</Button>
</SheetTrigger>
<SheetContent>
<SheetHeader>
<SheetTitle>User Details</SheetTitle>
<SheetDescription>Information about this user</SheetDescription>
</SheetHeader>
{user && (
<div className="mt-6 space-y-4">
<div>
<h4 className="text-sm font-medium text-muted-foreground">
Name
</h4>
<p className="text-base">{user.name}</p>
</div>
<div>
<h4 className="text-sm font-medium text-muted-foreground">
Email
</h4>
<p className="text-base">{user.email}</p>
</div>
{/* More fields... */}
</div>
)}
</SheetContent>
</Sheet>
);
}
```
---
## 10. Command Palette (Search)
```typescript
import {
Command,
CommandDialog,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import { useState, useEffect } from "react";
export default function GlobalSearch() {
const [open, setOpen] = useState(false);
useEffect(() => {
const down = (e: KeyboardEvent) => {
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
setOpen((open) => !open);
}
};
document.addEventListener("keydown", down);
return () => document.removeEventListener("keydown", down);
}, []);
return (
<CommandDialog open={open} onOpenChange={setOpen}>
<CommandInput placeholder="Search..." />
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup heading="Users">
<CommandItem>John Doe</CommandItem>
<CommandItem>Jane Smith</CommandItem>
</CommandGroup>
<CommandGroup heading="Projects">
<CommandItem>Project Alpha</CommandItem>
<CommandItem>Project Beta</CommandItem>
</CommandGroup>
</CommandList>
</CommandDialog>
);
}
```
---
For installation and design/layout, see:
- **Installation**: `SHADCN_INSTALLATION.md`
- **Design & layout**: `SHADCN_DESIGN_AND_LAYOUT.md`
- **Main guide index**: `SHADCN_COMPONENTS_GUIDE.md`

View File

@ -0,0 +1,91 @@
# shadcn/ui — Design & Layout
Design tokens, spacing, icons, responsive layouts, and performance tips for shadcn/ui dashboards.
---
## Design Tips
### Color Schemes
Use semantic color tokens:
- `bg-background` / `text-foreground` — Main background and text
- `bg-card` / `text-card-foreground` — Card surfaces
- `bg-primary` / `text-primary-foreground` — Primary actions
- `bg-destructive` — Destructive actions (delete, etc.)
- `bg-muted` / `text-muted-foreground` — Subtle UI elements
### Spacing
Use consistent spacing:
- `space-y-4` / `gap-4` — Between related items
- `space-y-6` / `gap-6` — Between sections
- `p-4` / `p-6` — Card padding
- `p-8` — Page padding
### Icons
Use `lucide-react` for consistent icons:
```typescript
import { Home, Users, Settings, Plus, Edit, Trash, Search } from "lucide-react";
<Button>
<Plus className="mr-2 h-4 w-4" />
Add Item
</Button>;
```
---
## Responsive Design
### Grid Layouts
```typescript
// 1 column on mobile, 2 on tablet, 4 on desktop
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
{/* cards */}
</div>
// 1 column on mobile, 3 on desktop
<div className="grid gap-6 lg:grid-cols-3">
{/* cards */}
</div>
```
### Hide on Mobile
```typescript
// Hide text on mobile, show on desktop
<span className="hidden sm:inline">Dashboard</span>
// Different layout on mobile
<div className="flex flex-col md:flex-row gap-4">
{/* content */}
</div>
```
---
## Performance Tips
1. **Lazy load dialogs** — Only render dialog content when open
2. **Virtualize long lists** — Use libraries like `react-window`
3. **Skeleton loaders** — Always show loading states
4. **Optimistic updates** — Update UI before server confirms
5. **Debounce search** — Don't query on every keystroke
---
**Remember**: Always test your components in both light and dark mode, and on different screen sizes.
---
For more, see:
- **Installation**: `SHADCN_INSTALLATION.md`
- **Dashboard patterns**: `SHADCN_DASHBOARD_PATTERNS.md`
- **Main guide index**: `SHADCN_COMPONENTS_GUIDE.md`

View File

@ -0,0 +1,29 @@
# shadcn/ui — Installation
How to add shadcn/ui components to your TaylorDB app.
---
## Install Individual Components
```bash
# Core components (often already included)
pnpm dlx shadcn@latest add button card input label textarea select tabs alert
# Dashboard-specific components
pnpm dlx shadcn@latest add table dialog dropdown-menu toast sheet form badge avatar skeleton
# Data display components
pnpm dlx shadcn@latest add separator progress scroll-area tooltip
# Advanced components
pnpm dlx shadcn@latest add command popover calendar date-picker
```
---
For patterns and examples, see:
- **Dashboard patterns**: `SHADCN_DASHBOARD_PATTERNS.md`
- **Design & layout**: `SHADCN_DESIGN_AND_LAYOUT.md`
- **Main guide index**: `SHADCN_COMPONENTS_GUIDE.md`

View File

@ -0,0 +1,117 @@
# TaylorDB Advanced Patterns
This document covers **advanced query patterns** using the TaylorDB query builder:
- Manual aggregations
- Summation helpers
- Conditional queries
- Pagination
---
## Aggregations (Manual)
Since TaylorDB query builder might not have built-in aggregations, compute manually:
```typescript
export async function getUserStats() {
const users = await queryBuilder
.selectFrom("users")
.select(["age"])
.execute();
if (users.length === 0) {
return { count: 0, average: null, min: null, max: null };
}
const ages = users
.map((u) => u.age)
.filter((a): a is number => a !== undefined);
return {
count: ages.length,
average: ages.reduce((a, b) => a + b, 0) / ages.length,
min: Math.min(...ages),
max: Math.max(...ages),
};
}
```
---
## Sum Totals
```typescript
export async function getTotalCaloriesForDate(date: string) {
const entries = await queryBuilder
.selectFrom("meals")
.select(["calories", "protein", "carbs", "fats"])
.where("date", "=", ["exactDay", date])
.execute();
return {
totalCalories: entries.reduce((sum, e) => sum + (e.calories ?? 0), 0),
totalProtein: entries.reduce((sum, e) => sum + (e.protein ?? 0), 0),
totalCarbs: entries.reduce((sum, e) => sum + (e.carbs ?? 0), 0),
totalFats: entries.reduce((sum, e) => sum + (e.fats ?? 0), 0),
};
}
```
---
## Conditional Queries
```typescript
export async function searchTasks(filters: {
projectId?: number;
status?: string;
dueAfter?: string;
}) {
let query = queryBuilder
.selectFrom("tasks")
.select(["id", "title", "status", "dueDate"]);
if (filters.projectId) {
query = query.where("projectId", "=", filters.projectId);
}
if (filters.status) {
query = query.where("status", "=", filters.status);
}
if (filters.dueAfter) {
query = query.where("dueDate", ">=", ["exactDay", filters.dueAfter]);
}
return await query.execute();
}
```
---
## Pagination
```typescript
export async function getPaginatedUsers(page: number, pageSize: number) {
const offset = (page - 1) * pageSize;
return await queryBuilder
.selectFrom("users")
.select(["id", "name", "email"])
.orderBy("createdAt", "desc")
.limit(pageSize)
.offset(offset)
.execute();
}
```
---
For more topics, see:
- `TAYLORDB_BASIC_QUERIES.md` for basic reads and filtering
- `TAYLORDB_WRITE_OPERATIONS.md` for inserts, updates, and deletes
- `TAYLORDB_FIELD_TYPES.md` for field type handling and enums
- `TAYLORDB_PITFALLS_BEST_PRACTICES.md` for pitfalls and best practices

View File

@ -0,0 +1,126 @@
# TaylorDB Attachments
Attachments are treated as **standard columns** and can be selected and written like other fields, using helper utilities for uploads.
This document covers:
- Selecting attachment fields
- Creating records with attachments
- Updating attachments
---
## Select Attachments
```typescript
// New Standard: Use regular .select() like any other field.
const expenses = await qb
.selectFrom("expenses")
.select(["id", "amount", "receipt"])
.execute();
```
---
## Create with Attachments
Use `qb.uploadAttachments` to upload files before inserting.
```typescript
await qb
.insertInto("customers")
.values({
firstName: "Jane",
lastName: "Doe",
avatar: await qb.uploadAttachments([
{ file: new Blob([""]), name: "test.png" },
]),
})
.execute();
```
---
## Update with Attachments
```typescript
await qb
.update("customers")
.set({
lastName: "Smith",
avatar: await qb.uploadAttachments([
{ file: new Blob([""]), name: "test.png" },
]),
})
.where("id", "=", 1)
.execute();
```
---
## Receiving Files via tRPC 11 Multipart FormData
tRPC 11 supports `multipart/form-data` natively using `z.instanceof(FormData)` as the procedure input. File objects arrive directly in the mutation — no separate upload endpoint needed.
**Server (tRPC router):**
```typescript
import { z } from "zod";
import { router, publicProcedure } from "../trpc";
export const myRouter = router({
submit: publicProcedure
.input(z.instanceof(FormData))
.mutation(async ({ input, ctx }) => {
const name = input.get("name") as string | null;
const files = input.getAll("files") as File[];
// Upload files directly to TaylorDB
const attachments = await ctx.queryBuilder.uploadAttachments(
files.map((file) => ({ file, name: file.name }))
);
await ctx.queryBuilder
.insertInto("submissions")
.values({ name, documents: attachments })
.execute();
}),
});
```
**Client (tRPC React Query):**
FormData mutations must bypass request batching. Use `splitLink` in your tRPC client setup:
```typescript
import { splitLink, httpLink, httpBatchLink } from "@trpc/client";
trpc.createClient({
links: [
splitLink({
condition: (op) => op.input instanceof FormData,
true: httpLink({ url: trpcUrl }),
false: httpBatchLink({ url: trpcUrl }),
}),
],
});
```
Then call the mutation with a `FormData` object:
```typescript
const formData = new FormData();
formData.append("name", name);
files.forEach((file) => formData.append("files", file));
await submitMutation.mutateAsync(formData);
```
For more topics, see:
- `TAYLORDB_BASIC_QUERIES.md` for basic reads and filtering
- `TAYLORDB_WRITE_OPERATIONS.md` for inserts, updates, and deletes
- `TAYLORDB_ADVANCED_PATTERNS.md` for aggregations, pagination, and conditional queries
- `TAYLORDB_FIELD_TYPES.md` for field type handling and enums
- `TAYLORDB_PITFALLS_BEST_PRACTICES.md` for pitfalls and best practices

View File

@ -0,0 +1,187 @@
# TaylorDB Basic Queries & Filtering
This document covers **core read operations** using the TaylorDB query builder:
- **Basic select queries**
- **Ordering**
- **Filtering with where clauses**
- **Date filters**
- **Array and select field filters**
- **Simple text search**
---
## Basic Queries
### Get All Records
```typescript
export async function getAllUsers() {
return await queryBuilder
.selectFrom("users")
.select(["id", "name", "email", "createdAt"])
.execute();
}
```
### Get All Records (All Fields)
```typescript
export async function getAllUsers() {
return await queryBuilder.selectFrom("users").execute();
}
```
### Get Single Record by ID
```typescript
export async function getUserById(id: number) {
return await queryBuilder
.selectFrom("users")
.where("id", "=", id)
.executeTakeFirst();
}
```
**Note**: `.executeTakeFirst()` returns a single record or `undefined`.
### Get Records with Ordering
```typescript
// Descending order (newest first)
export async function getRecentUsers() {
return await queryBuilder
.selectFrom("users")
.orderBy("createdAt", "desc")
.execute();
}
// Ascending order (oldest first)
export async function getOldestUsers() {
return await queryBuilder
.selectFrom("users")
.orderBy("createdAt", "asc")
.execute();
}
```
---
## Filtering & Conditions
### Basic Where Clauses
```typescript
// Exact match
.where("status", "=", "active")
// Not equal
.where("status", "!=", "deleted")
// Greater than / Less than
.where("age", ">", 18)
.where("age", ">=", 18)
.where("age", "<", 65)
.where("age", "<=", 65)
```
### Multiple Conditions (AND logic)
```typescript
export async function getActiveAdults() {
return await queryBuilder
.selectFrom("users")
.where("status", "=", "active")
.where("age", ">=", 18)
.execute();
}
```
### Date Filtering
```typescript
// Exact date
export async function getUsersForDate(date: string) {
return await queryBuilder
.selectFrom("users")
.where("createdAt", "=", ["exactDay", date])
.execute();
}
// Date range
export async function getUsersInRange(startDate: string, endDate: string) {
return await queryBuilder
.selectFrom("users")
.where("createdAt", ">=", ["exactDay", startDate])
.where("createdAt", "<=", ["exactDay", endDate])
.execute();
}
// Before/After a date
.where("dueDate", "<", ["exactDay", "2024-01-01"])
.where("startDate", ">", ["exactDay", "2024-12-31"])
```
### Array/Multi-Select Filtering
```typescript
// Check if array contains any of the values
export async function getUsersByTags(tags: string[]) {
return await queryBuilder
.selectFrom("users")
.where("tags", "hasAnyOf", tags)
.execute();
}
// Example: Get users tagged with "admin" OR "moderator"
const adminUsers = await getUsersByTags(["admin", "moderator"]);
```
### Select Field Filtering
#### Single Select
For single-select fields, the query builder now returns a single string value.
```typescript
export async function getUsersByRole(role: string) {
return await queryBuilder
.selectFrom("users")
.where("role", "=", role)
.execute();
}
```
#### Multi Select
For multi-select fields, the query builder returns and accepts multiple values.
```typescript
export async function getUsersByInterests(interests: string[]) {
return await queryBuilder
.selectFrom("users")
.where("interests", "hasAnyOf", interests)
.execute();
}
```
### Text Search (Contains)
```typescript
export async function searchUsersByName(query: string) {
return await queryBuilder
.selectFrom("users")
.where("name", "contains", query)
.execute();
}
```
---
For more topics, see:
- `TAYLORDB_WRITE_OPERATIONS.md` for inserts, updates, and deletes
- `TAYLORDB_ADVANCED_PATTERNS.md` for aggregations, pagination, and conditional queries
- `TAYLORDB_FIELD_TYPES.md` for field type handling and enums
- `TAYLORDB_PITFALLS_BEST_PRACTICES.md` for pitfalls and best practices

View File

@ -0,0 +1,84 @@
# TaylorDB Field Types & Enums
This document explains how TaylorDB field types map to TypeScript and how to handle them correctly:
- Field type reference
- Nullable fields
- Enums and options from generated types
---
## Field Type Reference
| TaylorDB Field Type | TypeScript Type | Insert Value | Query Value |
| ------------------- | ----------------------- | --------------------- | ---------------------------- |
| **Text** | `string` | `"Hello"` | `"Hello"` |
| **Number** | `number` | `42` | `42` |
| **Date** | `string` (ISO) | `"2024-01-15"` | `["exactDay", "2024-01-15"]` |
| **Checkbox** | `boolean` | `true` | `true` |
| **Single Select** | `string` | `"option"` | `"option"` |
| **Multi Select** | `string[]` | `["opt1", "opt2"]` | `["opt1", "opt2"]` |
| **Attachment** | `string[]` (File Paths) | `uploadAttachments()` | `"file-path"` |
| **Email** | `string` | `"user@example.com"` | `"user@example.com"` |
---
## Handling Nullable Fields
```typescript
export async function createUserSafe(data: {
name: string;
email?: string | null;
age?: number | null;
}) {
return await queryBuilder
.insertInto("users")
.values({
name: data.name,
email: data.email ?? "", // Default to empty string
age: data.age ?? 0, // Default to 0
})
.executeTakeFirst();
}
```
---
## Working with Enums
```typescript
// Import from generated types
import type { TaskStatusOptions } from "./types";
export async function createTask(data: {
title: string;
status: (typeof TaskStatusOptions)[number]; // e.g., "todo" | "in-progress" | "done"
}) {
return await queryBuilder
.insertInto("tasks")
.values({
title: data.title,
status: data.status,
})
.executeTakeFirst();
}
export async function getTasksByStatus(
status: (typeof TaskStatusOptions)[number],
) {
return await queryBuilder
.selectFrom("tasks")
.where("status", "=", status)
.execute();
}
```
---
For more topics, see:
- `TAYLORDB_BASIC_QUERIES.md` for basic reads and filtering
- `TAYLORDB_WRITE_OPERATIONS.md` for inserts, updates, and deletes
- `TAYLORDB_ADVANCED_PATTERNS.md` for aggregations, pagination, and conditional queries
- `TAYLORDB_PITFALLS_BEST_PRACTICES.md` for pitfalls and best practices

View File

@ -0,0 +1,98 @@
# TaylorDB Pitfalls & Best Practices
This document captures **common mistakes** and **recommended patterns** when using the TaylorDB query builder.
---
## Common Pitfalls
### ❌ Pitfall: Not Using exactDay for Dates
```typescript
// ❌ WRONG
.where("date", "=", "2024-01-15")
// ✅ CORRECT
.where("date", "=", ["exactDay", "2024-01-15"])
```
### ❌ Pitfall: Ignoring Nullable Fields
```typescript
// ❌ WRONG (assumes field is always present)
const user = await queryBuilder
.selectFrom("users")
.where("id", "=", 1)
.executeTakeFirst();
console.log(user.email); // Could be undefined!
// ✅ CORRECT
const user = await queryBuilder
.selectFrom("users")
.where("id", "=", 1)
.executeTakeFirst();
if (user && user.email) {
console.log(user.email);
}
```
### ❌ Pitfall: Using execute() for Single Record
```typescript
// ❌ WRONG (returns array)
const user = await queryBuilder
.selectFrom("users")
.where("id", "=", 1)
.execute();
console.log(user.name); // Error: user is an array!
// ✅ CORRECT
const user = await queryBuilder
.selectFrom("users")
.where("id", "=", 1)
.executeTakeFirst();
if (user) {
console.log(user.name);
}
```
### ❌ Pitfall: Not Handling Empty Arrays
```typescript
// ❌ WRONG (fails if users is empty)
const ages = users.map((u) => u.age);
const avg = ages.reduce((a, b) => a + b) / ages.length; // Division by zero!
// ✅ CORRECT
if (users.length === 0) {
return { average: null };
}
const ages = users
.map((u) => u.age)
.filter((a): a is number => a !== undefined);
const avg = ages.reduce((a, b) => a + b, 0) / ages.length;
```
---
## Best Practices
1. **Always handle `undefined` and `null`** when working with query results.
2. **Use TypeScript types** from `taylordb/types.ts` for type safety.
3. **Use `executeTakeFirst()`** when you expect a single record.
4. **Filter nullish values** before aggregations.
5. **Provide defaults** for optional fields.
6. **Use `["exactDay", date]`** format for date comparisons.
7. **Group related queries** in the same function file.
8. **Export functions**, not raw queries.
9. **Document complex queries** with JSDoc comments.
---
For more topics, see:
- `TAYLORDB_BASIC_QUERIES.md` for basic reads and filtering
- `TAYLORDB_WRITE_OPERATIONS.md` for inserts, updates, and deletes
- `TAYLORDB_ADVANCED_PATTERNS.md` for aggregations, pagination, and conditional queries
- `TAYLORDB_FIELD_TYPES.md` for field type handling and enums

View File

@ -0,0 +1,78 @@
# TaylorDB Query Builder Reference
This is the **entry point** for all TaylorDB query builder docs in this template.
The content has been split into smaller, focused files to make it easier for agents (and humans) to scan and reuse.
---
## 📚 Topics
- **Basic Reads & Filtering**
See `TAYLORDB_BASIC_QUERIES.md` for:
- Basic `selectFrom` usage
- Ordering
- `where` clauses
- Date filters
- Array/select field filters
- Text search (`contains`)
- **Write Operations (Insert, Update, Delete)**
See `TAYLORDB_WRITE_OPERATIONS.md` for:
- Inserting records (including single-/multi-select fields)
- Updates (single/multiple fields, conditional updates)
- Bulk updates
- Deleting single/multiple records and conditional deletes
- **Advanced Patterns**
See `TAYLORDB_ADVANCED_PATTERNS.md` for:
- Manual aggregations
- Summation helpers
- Conditional query builders
- Pagination patterns
- **Field Types & Enums**
See `TAYLORDB_FIELD_TYPES.md` for:
- TaylorDB field type → TypeScript mappings
- Nullable field handling
- Using generated enum options (`...Options`) types
- **Attachments**
See `TAYLORDB_ATTACHMENTS.md` for:
- Selecting attachment fields
- Creating/updating records with attachments via `uploadAttachments`
- **Pitfalls & Best Practices**
See `TAYLORDB_PITFALLS_BEST_PRACTICES.md` for:
- Common mistakes (e.g., forgetting `["exactDay", date]`, misusing `execute`)
- Recommended patterns for safe, type-accurate queries
---
## How Agents Should Use These Docs
1. **Start from your use case**
- Need a simple read? Open `TAYLORDB_BASIC_QUERIES.md`.
- Doing writes? Use `TAYLORDB_WRITE_OPERATIONS.md`.
- Need aggregations or pagination? Use `TAYLORDB_ADVANCED_PATTERNS.md`.
2. **Combine with generated types**
Always cross-reference:
- `apps/server/taylordb/types.ts` (schema-derived types)
- `apps/server/taylordb/query-builder.ts` (project-specific query functions)
3. **Check pitfalls before finalizing**
Before shipping queries, skim `TAYLORDB_PITFALLS_BEST_PRACTICES.md` to avoid common errors.
---
## Additional Resources
- **Generated Types**: `apps/server/taylordb/types.ts`
- **Example Queries in This Template**: `apps/server/taylordb/query-builder.ts`
- **tRPC Integration**: `apps/server/router.ts`
---
**Note**: These docs mirror the TaylorDB query builder patterns used in this template.
For the most up-to-date API details, always refer to the official TaylorDB documentation.

View File

@ -0,0 +1,265 @@
# TaylorDB Write Operations (Insert, Update, Delete)
This document covers **write operations** using the TaylorDB query builder:
- **Inserting data**
- **Updating single/multiple records**
- **Deleting records**
---
## Inserting Data
### Insert Single Record
```typescript
export async function createUser(data: {
name: string;
email: string;
age: number;
}) {
return await queryBuilder
.insertInto("users")
.values({
name: data.name,
email: data.email,
age: data.age,
status: "active", // Default value
})
.executeTakeFirst();
}
```
**Returns**: The created record with its generated `id`.
### Insert with Single-Select Field
Single-select fields now accept a single string value directly.
```typescript
export async function createTask(data: {
title: string;
priority: "low" | "medium" | "high";
}) {
return await queryBuilder
.insertInto("tasks")
.values({
title: data.title,
priority: data.priority,
})
.executeTakeFirst();
}
```
### Insert with Multi-Select Field
```typescript
export async function createProject(data: { name: string; tags: string[] }) {
return await queryBuilder
.insertInto("projects")
.values({
name: data.name,
tags: data.tags, // Already an array
})
.executeTakeFirst();
}
```
### Insert with Computed Fields
```typescript
export async function createCardioSession(data: {
distance: number;
duration: number; // in minutes
}) {
const speed = data.distance / (data.duration / 60); // km/h
return await queryBuilder
.insertInto("cardio")
.values({
distance: data.distance,
duration: data.duration,
speed: speed, // Computed field
})
.executeTakeFirst();
}
```
### Insert with Optional Fields
```typescript
export async function createPost(data: {
title: string;
content: string;
tags?: string[];
}) {
return await queryBuilder
.insertInto("posts")
.values({
title: data.title,
content: data.content,
tags: data.tags || [], // Default to empty array
})
.executeTakeFirst();
}
```
---
## Updating Data
### Update Single Field
```typescript
export async function updateUserName(id: number, name: string) {
return await queryBuilder
.update("users")
.set({ name })
.where("id", "=", id)
.execute();
}
```
### Update Multiple Fields
```typescript
export async function updateUser(
id: number,
data: {
name?: string;
email?: string;
age?: number;
},
) {
return await queryBuilder
.update("users")
.set(data)
.where("id", "=", id)
.execute();
}
```
**Note**: Only provided fields will be updated.
### Update with Single-Select Field
```typescript
export async function updateTaskPriority(
id: number,
priority: "low" | "medium" | "high",
) {
return await queryBuilder
.update("tasks")
.set({ priority })
.where("id", "=", id)
.execute();
}
```
### Update with Conditional Logic
```typescript
export async function updateCardioSession(
id: number,
data: {
distance?: number;
duration?: number;
},
) {
// Fetch current record to compute speed
const currentRecord = await queryBuilder
.selectFrom("cardio")
.select(["distance", "duration"])
.where("id", "=", id)
.executeTakeFirst();
if (!currentRecord) {
throw new Error("Record not found");
}
const newDistance = data.distance ?? currentRecord.distance ?? 0;
const newDuration = data.duration ?? currentRecord.duration ?? 0;
const speed = newDistance / (newDuration / 60);
return await queryBuilder
.update("cardio")
.set({
...data,
speed,
})
.where("id", "=", id)
.execute();
}
```
### Update Multiple Records
```typescript
export async function activateAllUsers() {
return await queryBuilder.update("users").set({ status: "active" }).execute(); // No where clause = update all
}
// Update with condition
export async function activateInactiveUsers() {
return await queryBuilder
.update("users")
.set({ status: "active" })
.where("status", "=", "inactive")
.execute();
}
```
---
## Deleting Data
### Delete Single Record
```typescript
export async function deleteUser(id: number) {
return await queryBuilder.deleteFrom("users").where("id", "=", id).execute();
}
```
### Delete Multiple Records by IDs
```typescript
export async function deleteUsers(ids: number[]) {
return await queryBuilder
.deleteFrom("users")
.where("id", "hasAnyOf", ids)
.execute();
}
```
### Delete with Condition
```typescript
export async function deleteInactiveUsers() {
return await queryBuilder
.deleteFrom("users")
.where("status", "=", "inactive")
.execute();
}
```
### Delete Old Records
```typescript
export async function deleteOldLogs(beforeDate: string) {
return await queryBuilder
.deleteFrom("logs")
.where("createdAt", "<", ["exactDay", beforeDate])
.execute();
}
```
---
For more topics, see:
- `TAYLORDB_BASIC_QUERIES.md` for basic reads and filtering
- `TAYLORDB_ADVANCED_PATTERNS.md` for aggregations, pagination, and conditional queries
- `TAYLORDB_FIELD_TYPES.md` for field type handling and enums
- `TAYLORDB_PITFALLS_BEST_PRACTICES.md` for pitfalls and best practices

24
eslint.config.js Normal file
View File

@ -0,0 +1,24 @@
import js from '@eslint/js'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import { defineConfig, globalIgnores } from 'eslint/config'
import globals from 'globals'
import tseslint from 'typescript-eslint'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
ignores: ['dist', './src/lib/*.ts'],
},
])

20
package.json Normal file
View File

@ -0,0 +1,20 @@
{
"name": "taylordb-monorepo",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "pnpm --filter @repo/client dev",
"dev:server": "pnpm --filter @repo/server dev",
"dev:full": "concurrently \"pnpm dev\" \"pnpm dev:server\" --names \"client,server\" --prefix-colors \"cyan,magenta\"",
"build": "pnpm --filter @repo/client build",
"build:server": "pnpm --filter @repo/server build",
"build:all": "pnpm build && pnpm build:server",
"lint": "eslint apps",
"generate:schema": "pnpx @taylordb/cli generate-schema"
},
"devDependencies": {
"concurrently": "^9.2.1",
"eslint": "^9.39.1",
"typescript": "~5.9.3"
}
}

5245
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

4
pnpm-workspace.yaml Normal file
View File

@ -0,0 +1,4 @@
packages:
- apps/*
dangerouslyAllowAllBuilds: true

86
taylordb.yml Normal file
View File

@ -0,0 +1,86 @@
# yaml-language-server: $schema=https://server-vms.develop.taylordb.ai/apps/config-schema/v1
version: 1
project: blank
runtime:
type: node
version: 20
packageManager: pnpm
services:
server:
workDir: apps/server
install:
commands:
- pnpm install
dev:
command: pnpm dev
port: 3000
env:
vars:
TAYLORDB_BASE_URL: vars.TAYLORDB_INTERNAL_BASE_URL
TAYLORDB_SERVER_ID: vars.TAYLORDB_SERVER_ID
FRONTEND_URL: routing.client.url
start:
command: pnpm start --port 3000
port: 3000
env:
vars:
TAYLORDB_BASE_URL: vars.TAYLORDB_INTERNAL_BASE_URL
TAYLORDB_SERVER_ID: vars.TAYLORDB_SERVER_ID
FRONTEND_URL: routing.client.url
build:
commands:
- pnpm build
env:
vars:
TAYLORDB_BASE_URL: vars.TAYLORDB_INTERNAL_BASE_URL
TAYLORDB_SERVER_ID: vars.TAYLORDB_SERVER_ID
FRONTEND_URL: routing.client.url
client:
workDir: apps/client
publicEnvPrefix: VITE_
install:
commands:
- pnpm install
dev:
command: pnpm dev
port: 5173
env:
vars:
TRPC_URL: routing.server.url
start:
command: pnpm preview --port 5173
port: 5173
env:
vars:
TRPC_URL: routing.server.url
build:
commands:
- pnpm build
env:
vars:
TRPC_URL: routing.server.url
taylordb:
version: 0.14.0
types:
- output: apps/server/taylordb/types.ts
format: typescript
onChange:
- pnpm lint
preview:
service: client
routing:
- path: /api
service: server
- path: /
service: client

4
tsconfig.json Normal file
View File

@ -0,0 +1,4 @@
{
"files": [],
"references": [{ "path": "./apps/client" }, { "path": "./apps/server" }]
}