init mono repo and trpc
This commit is contained in:
parent
0fb69ae14a
commit
420c8e7d3a
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -10,6 +10,7 @@ lerna-debug.log*
|
|||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
server/dist
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
|
|
@ -24,6 +25,7 @@ dist-ssr
|
|||
*.sw?
|
||||
|
||||
.env*
|
||||
!.env.example
|
||||
.aider*
|
||||
|
||||
src/lib/taylordb.types.ts
|
||||
325
README.md
325
README.md
|
|
@ -1,45 +1,310 @@
|
|||
# TaylorDB React Starter
|
||||
# TaylorDB + tRPC Full-Stack Monorepo
|
||||
|
||||
React + Vite starter with Tailwind CSS (v4), React Router v6, and shadcn/ui wired up for building custom UIs on TaylorDB.
|
||||
A production-ready pnpm monorepo combining React frontend and Express backend with tRPC for type-safe API communication. Built for AI-assisted development platforms.
|
||||
|
||||
## What's included
|
||||
- React Router layout with sample pages (`/`, `/about`, fallback `*`)
|
||||
- Tailwind v4 configured for shadcn (design tokens, dark mode, animations)
|
||||
- shadcn/ui baseline components: `button`, `card`, `input`, `label`, `textarea`, `select`, `tabs`, `alert`
|
||||
- Path aliases via `@` (`@/components`, `@/lib`, etc.)
|
||||
## 📦 Monorepo Structure
|
||||
|
||||
```
|
||||
taylordb-clientserver-template/
|
||||
├── pnpm-workspace.yaml # Workspace configuration
|
||||
├── package.json # @repo/frontend (root)
|
||||
├── server/
|
||||
│ ├── package.json # @repo/server
|
||||
│ ├── index.ts # Express + tRPC server
|
||||
│ ├── router.ts # tRPC procedures
|
||||
│ ├── trpc.ts # tRPC configuration
|
||||
│ └── taylordb/
|
||||
│ ├── types.ts # Auto-generated TaylorDB types
|
||||
│ └── query-builder.ts # TaylorDB CRUD operations
|
||||
├── src/ # React frontend
|
||||
└── ...
|
||||
```
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
## Getting started
|
||||
```bash
|
||||
# Install all workspace dependencies
|
||||
pnpm install
|
||||
pnpm dev
|
||||
|
||||
# Run both frontend and backend
|
||||
pnpm dev:full
|
||||
|
||||
# Or run separately
|
||||
pnpm dev # Frontend only (port 5173)
|
||||
pnpm dev:server # Backend only (port 3001)
|
||||
```
|
||||
|
||||
## Using the shadcn CLI
|
||||
The project is already initialized with `components.json`. Add more components with:
|
||||
Visit:
|
||||
|
||||
- **Frontend**: http://localhost:5173
|
||||
- **Backend**: http://localhost:3001
|
||||
- **Demo Page**: http://localhost:5173/trpc-demo
|
||||
|
||||
## 📚 Workspaces
|
||||
|
||||
### @repo/frontend (Root)
|
||||
|
||||
The React + Vite frontend application
|
||||
|
||||
**Location**: `/` (root directory)
|
||||
|
||||
**Tech Stack**:
|
||||
|
||||
- React 19 with TypeScript
|
||||
- Vite 7 for bundling
|
||||
- TailwindCSS 4 for styling
|
||||
- shadcn/ui components
|
||||
- React Router v6
|
||||
- tRPC React Query client
|
||||
|
||||
**Scripts**:
|
||||
|
||||
```bash
|
||||
pnpm dlx shadcn@latest add <component>
|
||||
pnpm dev # Start dev server
|
||||
pnpm build # Build for production
|
||||
pnpm lint # Run ESLint
|
||||
```
|
||||
Examples:
|
||||
|
||||
### @repo/server
|
||||
|
||||
The Express + tRPC backend API
|
||||
|
||||
**Location**: `/server`
|
||||
|
||||
**Tech Stack**:
|
||||
|
||||
- Express 5 web server
|
||||
- tRPC 11 for type-safe APIs
|
||||
- TaylorDB query builder
|
||||
- Zod for validation
|
||||
- TypeScript 5
|
||||
|
||||
**Scripts**:
|
||||
|
||||
```bash
|
||||
pnpm dlx shadcn@latest add dialog dropdown-menu table
|
||||
pnpm --filter @repo/server dev # Start with hot reload
|
||||
pnpm --filter @repo/server build # Build TypeScript
|
||||
pnpm --filter @repo/server start # Run production build
|
||||
```
|
||||
Generated files go into `src/components/ui/` and use the shared Tailwind tokens in `src/index.css`.
|
||||
|
||||
### Available components
|
||||
- Layout/structure: `card`, `tabs`
|
||||
- Form controls: `input`, `label`, `textarea`, `select`
|
||||
- Feedback: `alert`
|
||||
- Buttons: `button`
|
||||
## 🛠️ Available Scripts
|
||||
|
||||
## TaylorDB integration
|
||||
Use the generated TaylorDB client and types (expected in `src/lib/taylordb.client.ts` and `src/lib/taylordb.types.ts`) to query data directly. Do not use mock data.
|
||||
### Root Commands (run from anywhere)
|
||||
|
||||
## Scripts
|
||||
- `pnpm dev` — run Vite dev server (HMR)
|
||||
- `pnpm build` — type-check + build
|
||||
- `pnpm lint` — ESLint (strict, no `any`)
|
||||
```bash
|
||||
# Development
|
||||
pnpm dev:full # Run both workspaces concurrently
|
||||
pnpm dev # Frontend only
|
||||
pnpm dev:server # Backend only
|
||||
|
||||
## Notes
|
||||
- Tailwind 4 uses `@tailwindcss/vite`; CSS entry is `@import "tailwindcss";` in `src/index.css`.
|
||||
- Tailwind config is in `tailwind.config.js`; CSS tokens live in `src/index.css`.
|
||||
- Components use `@/lib/utils` for the `cn` helper (clsx + tailwind-merge).
|
||||
# Building
|
||||
pnpm build # Build frontend
|
||||
pnpm build:server # Build backend
|
||||
pnpm build:all # Build both
|
||||
|
||||
# Other
|
||||
pnpm lint # Lint all code
|
||||
pnpm generate:schema # Generate TaylorDB types
|
||||
```
|
||||
|
||||
### Workspace-Specific Commands
|
||||
|
||||
```bash
|
||||
# Run command in specific workspace
|
||||
pnpm --filter @repo/frontend <command>
|
||||
pnpm --filter @repo/server <command>
|
||||
|
||||
# Examples
|
||||
pnpm --filter @repo/server dev
|
||||
pnpm --filter @repo/frontend build
|
||||
```
|
||||
|
||||
## 🔧 Configuration Files
|
||||
|
||||
### pnpm-workspace.yaml
|
||||
|
||||
Defines workspace packages:
|
||||
|
||||
```yaml
|
||||
packages:
|
||||
- "server"
|
||||
- "."
|
||||
```
|
||||
|
||||
### Package Names
|
||||
|
||||
- Frontend: `@repo/frontend`
|
||||
- Backend: `@repo/server`
|
||||
|
||||
All packages are marked as `private: true` (not published to npm)
|
||||
|
||||
## 📡 API Communication
|
||||
|
||||
### tRPC Router Export
|
||||
|
||||
The backend exports its router type from `server/router.ts`:
|
||||
|
||||
```typescript
|
||||
export type AppRouter = typeof appRouter;
|
||||
```
|
||||
|
||||
### Frontend tRPC Client
|
||||
|
||||
The frontend imports the type for end-to-end type safety:
|
||||
|
||||
```typescript
|
||||
import type { AppRouter } from "../../server/router";
|
||||
export const trpc = createTRPCReact<AppRouter>();
|
||||
```
|
||||
|
||||
**Note**: This works because both packages are in the same monorepo, allowing direct TypeScript imports without shared packages.
|
||||
|
||||
## 🗄️ TaylorDB Integration
|
||||
|
||||
### Setup
|
||||
|
||||
1. Copy `.env.example` to `.env`
|
||||
2. Add your TaylorDB credentials:
|
||||
```bash
|
||||
TAYLORDB_BASE_URL=your_base_url
|
||||
TAYLORDB_BASE_ID=your_base_id
|
||||
TAYLORDB_API_KEY=your_api_key
|
||||
```
|
||||
3. Generate types: `pnpm generate:schema`
|
||||
|
||||
### CRUD Operations
|
||||
|
||||
50+ type-safe procedures available:
|
||||
|
||||
- **Weight Tracking**: Full CRUD + statistics
|
||||
- **Goals Management**: Create, read, update, delete
|
||||
- **Strength Workouts**: Exercise logging
|
||||
- **Cardio Exercises**: Duration, distance, speed tracking
|
||||
- **Calories/Nutrition**: Daily totals and aggregations
|
||||
- **Settings**: Key-value configuration
|
||||
|
||||
See [TAYLORDB_INTEGRATION.md](file:///Users/thetaung/Desktop/taylordb-clientserver-template/TAYLORDB_INTEGRATION.md) for detailed examples.
|
||||
|
||||
## 🏗️ Monorepo Benefits
|
||||
|
||||
### For This Project
|
||||
|
||||
✅ **Shared Types**: Frontend and backend share TypeScript types directly
|
||||
✅ **Single Repository**: One git repo, one pnpm-lock.yaml
|
||||
✅ **Coordinated Builds**: Build both projects together
|
||||
✅ **Simplified Setup**: One `pnpm install` for everything
|
||||
✅ **Workspace Commands**: Target specific packages easily
|
||||
|
||||
### Compared to Separate Repos
|
||||
|
||||
- ⚡ Faster development (no publishing to npm for type updates)
|
||||
- 🔒 Type safety guaranteed at development time
|
||||
- 📝 Simpler dependency management
|
||||
- 🔄 Atomic commits across frontend and backend
|
||||
|
||||
## 📦 Dependency Management
|
||||
|
||||
### Installing Dependencies
|
||||
|
||||
**For frontend**:
|
||||
|
||||
```bash
|
||||
# From root
|
||||
pnpm add <package>
|
||||
|
||||
# Or explicitly
|
||||
pnpm --filter @repo/frontend add <package>
|
||||
```
|
||||
|
||||
**For backend**:
|
||||
|
||||
```bash
|
||||
pnpm --filter @repo/server add <package>
|
||||
```
|
||||
|
||||
**For both**:
|
||||
|
||||
```bash
|
||||
pnpm add <package> -w # Add to root workspace
|
||||
```
|
||||
|
||||
### Workspace Dependencies
|
||||
|
||||
Workspaces can depend on each other using `workspace:*`:
|
||||
|
||||
```json
|
||||
{
|
||||
"dependencies": {
|
||||
"@repo/shared": "workspace:*"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🚢 Deployment
|
||||
|
||||
### Option 1: Deploy Separately
|
||||
|
||||
- Frontend → Vercel/Netlify (static site from `dist/`)
|
||||
- Backend → Railway/Render/Fly.io (Node.js app)
|
||||
|
||||
Build commands:
|
||||
|
||||
```bash
|
||||
pnpm build # Frontend
|
||||
pnpm build:server # Backend
|
||||
```
|
||||
|
||||
### Option 2: Deploy Together
|
||||
|
||||
Serve frontend from backend:
|
||||
|
||||
```bash
|
||||
pnpm build:all # Build both
|
||||
# Configure Express to serve frontend static files
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Set these in your deployment platform:
|
||||
|
||||
- `TAYLORDB_BASE_URL`
|
||||
- `TAYLORDB_BASE_ID`
|
||||
- `TAYLORDB_API_KEY`
|
||||
- `FRONTEND_URL` (for CORS)
|
||||
- `VITE_TRPC_URL` (frontend)
|
||||
|
||||
## 🎯 Next Steps
|
||||
|
||||
1. **Add Shared Package** (optional):
|
||||
|
||||
```bash
|
||||
mkdir packages/shared
|
||||
# Create package.json for shared utilities/types
|
||||
```
|
||||
|
||||
2. **Add Tests**:
|
||||
|
||||
```bash
|
||||
pnpm --filter @repo/server add -D vitest
|
||||
pnpm --filter @repo/frontend add -D vitest
|
||||
```
|
||||
|
||||
3. **CI/CD**:
|
||||
- Use `pnpm install --frozen-lockfile` in CI
|
||||
- Run `pnpm build:all` for deployment
|
||||
- Cache `node_modules` and `.pnpm-store`
|
||||
|
||||
## 📚 Learn More
|
||||
|
||||
- [pnpm Workspaces](https://pnpm.io/workspaces)
|
||||
- [tRPC Documentation](https://trpc.io)
|
||||
- [TaylorDB](https://taylordb.io)
|
||||
- [Monorepo Handbook](https://monorepo.tools)
|
||||
|
||||
## 📄 License
|
||||
|
||||
MIT - Use freely for any project!
|
||||
|
||||
---
|
||||
|
||||
**Built with ❤️ for AI-assisted development platforms**
|
||||
|
|
|
|||
234
TAYLORDB_INTEGRATION.md
Normal file
234
TAYLORDB_INTEGRATION.md
Normal file
|
|
@ -0,0 +1,234 @@
|
|||
# TaylorDB Integration Summary
|
||||
|
||||
## Created Files
|
||||
|
||||
### [server/taylordb/query-builder.ts](file:///Users/thetaung/Desktop/taylordb-clientserver-template/server/taylordb/query-builder.ts)
|
||||
|
||||
Comprehensive CRUD operations for all TaylorDB tables with **40+ functions**:
|
||||
|
||||
#### Weight Tracking
|
||||
|
||||
- ✅ `getAllWeightRecords()` - Get all weight entries
|
||||
- ✅ `getWeightRecordsByDateRange()` - Filter by date range
|
||||
- ✅ `getWeightRecordById()` - Get single record
|
||||
- ✅ `createWeightRecord()` - Add new weight entry
|
||||
- ✅ `updateWeightRecord()` - Update existing record
|
||||
- ✅ `deleteWeightRecord()` - Delete single record
|
||||
- ✅ `deleteWeightRecords()` - Batch delete
|
||||
- ✅ `getWeightStats()` - Get aggregated statistics
|
||||
|
||||
#### Goals Management
|
||||
|
||||
- ✅ `getAllGoals()` - List all goals
|
||||
- ✅ `createGoal()` - Create new goal
|
||||
- ✅ `updateGoal()` - Update goal
|
||||
- ✅ `deleteGoal()` - Delete goal
|
||||
|
||||
#### Strength Training
|
||||
|
||||
- ✅ `getStrengthWorkouts()` - Get workouts with optional filtering
|
||||
- ✅ `createStrengthWorkout()` - Log workout
|
||||
- ✅ `updateStrengthWorkout()` - Update workout
|
||||
- ✅ `deleteStrengthWorkout()` - Delete workout
|
||||
|
||||
#### Cardio Exercise
|
||||
|
||||
- ✅ `getCardioExercises()` - Get all cardio sessions
|
||||
- ✅ `createCardioExercise()` - Log cardio session (auto-calculates speed)
|
||||
- ✅ `updateCardioExercise()` - Update session
|
||||
- ✅ `deleteCardioExercise()` - Delete session
|
||||
|
||||
#### Calories/Nutrition
|
||||
|
||||
- ✅ `getCaloriesByTimeOfDay()` - Filter by meal time
|
||||
- ✅ `createCalorieEntry()` - Log meal/calories
|
||||
- ✅ `updateCalorieEntry()` - Update nutrition data
|
||||
- ✅ `deleteCalorieEntry()` - Delete entry
|
||||
- ✅ `getTotalCaloriesForDate()` - Daily nutrition aggregation
|
||||
|
||||
#### Settings
|
||||
|
||||
- ✅ `getSettingByName()` - Get setting by name
|
||||
- ✅ `createSetting()` - Create/update setting
|
||||
- ✅ `updateSetting()` - Modify setting
|
||||
- ✅ `deleteSetting()` - Remove setting
|
||||
|
||||
---
|
||||
|
||||
### [server/router.ts](file:///Users/thetaung/Desktop/taylordb-clientserver-template/server/router.ts)
|
||||
|
||||
Updated tRPC router with **50+ type-safe procedures** organized by feature:
|
||||
|
||||
```typescript
|
||||
appRouter = {
|
||||
hello: { ... }, // Test procedure
|
||||
|
||||
weight: {
|
||||
getAll: query,
|
||||
getById: query,
|
||||
getByDateRange: query,
|
||||
create: mutation,
|
||||
update: mutation,
|
||||
delete: mutation,
|
||||
deleteMultiple: mutation,
|
||||
getStats: query,
|
||||
},
|
||||
|
||||
goals: {
|
||||
getAll: query,
|
||||
create: mutation,
|
||||
update: mutation,
|
||||
delete: mutation,
|
||||
},
|
||||
|
||||
strength: {
|
||||
getAll: query,
|
||||
create: mutation,
|
||||
update: mutation,
|
||||
delete: mutation,
|
||||
},
|
||||
|
||||
cardio: {
|
||||
getAll: query,
|
||||
create: mutation,
|
||||
update: mutation,
|
||||
delete: mutation,
|
||||
},
|
||||
|
||||
calories: {
|
||||
getByTimeOfDay: query,
|
||||
create: mutation,
|
||||
update: mutation,
|
||||
delete: mutation,
|
||||
getTotalForDate: query,
|
||||
},
|
||||
|
||||
settings: {
|
||||
getByName: query,
|
||||
create: mutation,
|
||||
update: mutation,
|
||||
delete: mutation,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
**All procedures include**:
|
||||
|
||||
- ✅ Zod validation
|
||||
- ✅ Type safety
|
||||
- ✅ Error handling
|
||||
- ✅ Proper TypeScript types
|
||||
|
||||
---
|
||||
|
||||
### [.env.example](file:///Users/thetaung/Desktop/taylordb-clientserver-template/.env.example)
|
||||
|
||||
Environment configuration template with all required variables.
|
||||
|
||||
---
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Frontend (React Component)
|
||||
|
||||
```typescript
|
||||
import { trpc } from "@/lib/trpc";
|
||||
|
||||
function WeightTracker() {
|
||||
// Query all weights
|
||||
const { data: weights } = trpc.weight.getAll.useQuery();
|
||||
|
||||
// Create mutation
|
||||
const createWeight = trpc.weight.create.useMutation({
|
||||
onSuccess: () => {
|
||||
// Refetch or invalidate queries
|
||||
},
|
||||
});
|
||||
|
||||
// Add new weight entry
|
||||
const handleSubmit = () => {
|
||||
createWeight.mutate({
|
||||
date: "2026-01-08",
|
||||
weight: 75.5,
|
||||
name: "Morning weight",
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{weights?.map((w) => (
|
||||
<div key={w.id}>
|
||||
{w.date}: {w.weight}kg
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Backend (Direct Query Builder Usage)
|
||||
|
||||
```typescript
|
||||
import * as db from "./taylordb/query-builder";
|
||||
|
||||
// Get workouts for specific exercise
|
||||
const pushups = await db.getStrengthWorkouts("Push-ups");
|
||||
|
||||
// Create cardio session with auto-calculated speed
|
||||
await db.createCardioExercise({
|
||||
exercise: "Running",
|
||||
duration: 30, // minutes
|
||||
distance: 5, // km
|
||||
date: "2026-01-08",
|
||||
// speed is automatically calculated: 5km / 0.5hr = 10km/h
|
||||
});
|
||||
|
||||
// Get nutrition totals for today
|
||||
const nutrition = await db.getTotalCaloriesForDate("2026-01-08");
|
||||
console.log(nutrition.totalCalories.sum); // e.g., 2000
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Features
|
||||
|
||||
### Type Safety
|
||||
|
||||
- Full end-to-end type inference
|
||||
- TypeScript autocomplete for all operations
|
||||
- Zod runtime validation
|
||||
|
||||
### Query Builder Features
|
||||
|
||||
- **Filtering**: Date ranges, text search, numeric comparisons
|
||||
- **Aggregations**: Sum, average, min/max, statistics
|
||||
- **Batch Operations**: Delete multiple records
|
||||
- **Auto-calculations**: Speed calculation for cardio
|
||||
|
||||
### Organization
|
||||
|
||||
- Grouped by domain (weight, goals, strength, etc.)
|
||||
- Clear naming conventions
|
||||
- Comprehensive documentation
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Copy `.env.example` to `.env`** and add your TaylorDB credentials
|
||||
2. **Generate types**: Run `pnpm generate:schema` to create TaylorDB types
|
||||
3. **Test endpoints**: Use the tRPC demo page or create your own
|
||||
4. **Add authentication**: Protect procedures as needed
|
||||
5. **Customize**: Add more procedures for your specific use cases
|
||||
|
||||
---
|
||||
|
||||
## TypeScript Notes
|
||||
|
||||
The type errors you see are expected until you:
|
||||
|
||||
1. Set up TaylorDB credentials in `.env`
|
||||
2. Run `pnpm generate:schema` to generate proper types
|
||||
3. Restart the TypeScript server
|
||||
|
||||
The code is ready to use - the types will align once TaylorDB is properly configured!
|
||||
54
apps/client/package.json
Normal file
54
apps/client/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
|
@ -8,6 +8,7 @@ import { cn } from "@/lib/utils";
|
|||
const navItems = [
|
||||
{ to: "/", label: "Home" },
|
||||
{ to: "/about", label: "About" },
|
||||
{ to: "/trpc-demo", label: "tRPC Demo" },
|
||||
];
|
||||
|
||||
const getInitialTheme = (): "light" | "dark" => {
|
||||
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.0 KiB |
38
apps/client/src/lib/trpc.ts
Normal file
38
apps/client/src/lib/trpc.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { createTRPCReact, type CreateTRPCReact } from "@trpc/react-query";
|
||||
import { httpBatchLink, 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
|
||||
*/
|
||||
function getBaseUrl() {
|
||||
if (typeof window !== "undefined") {
|
||||
// Browser: use relative URL or environment variable
|
||||
return import.meta.env.VITE_TRPC_URL || "http://localhost:3001";
|
||||
}
|
||||
// SSR: assume localhost
|
||||
return "http://localhost:3001";
|
||||
}
|
||||
|
||||
/**
|
||||
* Create tRPC client
|
||||
*/
|
||||
export const trpcClient: TRPCClient<AppRouter> = trpc.createClient({
|
||||
links: [
|
||||
httpBatchLink({
|
||||
url: `${getBaseUrl()}/trpc`,
|
||||
// Optional: add headers, credentials, etc.
|
||||
// headers() {
|
||||
// return {
|
||||
// authorization: getAuthToken(),
|
||||
// };
|
||||
// },
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
|
@ -1,12 +1,24 @@
|
|||
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 AboutPage from "./pages/AboutPage";
|
||||
import HomePage from "./pages/HomePage";
|
||||
import NotFoundPage from "./pages/NotFoundPage";
|
||||
import TRPCDemoPage from "./pages/TRPCDemoPage";
|
||||
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([
|
||||
{
|
||||
|
|
@ -15,6 +27,7 @@ const router = createBrowserRouter([
|
|||
children: [
|
||||
{ index: true, element: <HomePage /> },
|
||||
{ path: "about", element: <AboutPage /> },
|
||||
{ path: "trpc-demo", element: <TRPCDemoPage /> },
|
||||
],
|
||||
},
|
||||
{ path: "*", element: <NotFoundPage /> },
|
||||
|
|
@ -22,6 +35,10 @@ const router = createBrowserRouter([
|
|||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<trpc.Provider client={trpcClient} queryClient={queryClient}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<RouterProvider router={router} />
|
||||
</QueryClientProvider>
|
||||
</trpc.Provider>
|
||||
</StrictMode>
|
||||
);
|
||||
603
apps/client/src/pages/TRPCDemoPage.tsx
Normal file
603
apps/client/src/pages/TRPCDemoPage.tsx
Normal file
|
|
@ -0,0 +1,603 @@
|
|||
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 { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
|
||||
export default function TRPCDemoPage() {
|
||||
return (
|
||||
<div className="container mx-auto p-8 max-w-6xl">
|
||||
<h1 className="text-4xl font-bold mb-2">TaylorDB + tRPC Demo</h1>
|
||||
<p className="text-muted-foreground mb-8">
|
||||
Full-stack CRUD operations with type safety
|
||||
</p>
|
||||
|
||||
<Tabs defaultValue="weight" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-5">
|
||||
<TabsTrigger value="weight">Weight</TabsTrigger>
|
||||
<TabsTrigger value="goals">Goals</TabsTrigger>
|
||||
<TabsTrigger value="strength">Strength</TabsTrigger>
|
||||
<TabsTrigger value="cardio">Cardio</TabsTrigger>
|
||||
<TabsTrigger value="calories">Calories</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="weight">
|
||||
<WeightTracker />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="goals">
|
||||
<GoalsManager />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="strength">
|
||||
<StrengthWorkouts />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="cardio">
|
||||
<CardioExercises />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="calories">
|
||||
<CaloriesTracker />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Weight Tracker Component
|
||||
// ============================================================================
|
||||
|
||||
function WeightTracker() {
|
||||
const [weight, setWeight] = useState("");
|
||||
const [date, setDate] = useState(new Date().toISOString().split("T")[0]);
|
||||
|
||||
const { data: weights, isLoading, refetch } = trpc.weight.getAll.useQuery();
|
||||
const { data: stats } = trpc.weight.getStats.useQuery();
|
||||
|
||||
const createMutation = trpc.weight.create.useMutation({
|
||||
onSuccess: () => {
|
||||
refetch();
|
||||
setWeight("");
|
||||
},
|
||||
});
|
||||
|
||||
const deleteMutation = trpc.weight.delete.useMutation({
|
||||
onSuccess: () => refetch(),
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (weight) {
|
||||
createMutation.mutate({
|
||||
date,
|
||||
weight: parseFloat(weight),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid gap-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Weight Statistics</CardTitle>
|
||||
<CardDescription>Your weight tracking overview</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{stats && (
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<div className="text-center">
|
||||
<p className="text-2xl font-bold">{stats.count}</p>
|
||||
<p className="text-sm text-muted-foreground">Total Entries</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-2xl font-bold">
|
||||
{stats.average?.toFixed(1) || "-"}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">Average (kg)</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-2xl font-bold">
|
||||
{stats.min?.toFixed(1) || "-"}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">Min (kg)</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-2xl font-bold">
|
||||
{stats.max?.toFixed(1) || "-"}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">Max (kg)</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Add Weight Entry</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="date">Date</Label>
|
||||
<Input
|
||||
id="date"
|
||||
type="date"
|
||||
value={date}
|
||||
onChange={(e) => setDate(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="weight">Weight (kg)</Label>
|
||||
<Input
|
||||
id="weight"
|
||||
type="number"
|
||||
step="0.1"
|
||||
value={weight}
|
||||
onChange={(e) => setWeight(e.target.value)}
|
||||
placeholder="75.5"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Button type="submit" disabled={createMutation.isPending}>
|
||||
{createMutation.isPending ? "Adding..." : "Add Entry"}
|
||||
</Button>
|
||||
{createMutation.error && (
|
||||
<p className="text-sm text-destructive">
|
||||
{createMutation.error.message}
|
||||
</p>
|
||||
)}
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Weight History</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{isLoading && <p>Loading...</p>}
|
||||
{weights && weights.length === 0 && (
|
||||
<p className="text-muted-foreground">No entries yet</p>
|
||||
)}
|
||||
<div className="space-y-2">
|
||||
{weights?.map((w) => (
|
||||
<div
|
||||
key={w.id}
|
||||
className="flex items-center justify-between p-3 border rounded-lg"
|
||||
>
|
||||
<div>
|
||||
<p className="font-medium">{w.weight} kg</p>
|
||||
<p className="text-sm text-muted-foreground">{w.date}</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={() => w.id && deleteMutation.mutate({ id: w.id })}
|
||||
disabled={deleteMutation.isPending}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Goals Manager Component
|
||||
// ============================================================================
|
||||
|
||||
function GoalsManager() {
|
||||
const [name, setName] = useState("");
|
||||
const [value, setValue] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
|
||||
const { data: goals, isLoading, refetch } = trpc.goals.getAll.useQuery();
|
||||
|
||||
const createMutation = trpc.goals.create.useMutation({
|
||||
onSuccess: () => {
|
||||
refetch();
|
||||
setName("");
|
||||
setValue("");
|
||||
setDescription("");
|
||||
},
|
||||
});
|
||||
|
||||
const deleteMutation = trpc.goals.delete.useMutation({
|
||||
onSuccess: () => refetch(),
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
createMutation.mutate({ name, value, description });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid gap-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Create Goal</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="goalName">Goal Name</Label>
|
||||
<Input
|
||||
id="goalName"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Lose 5kg"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="goalValue">Target Value</Label>
|
||||
<Input
|
||||
id="goalValue"
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
placeholder="70kg"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="goalDescription">Description (optional)</Label>
|
||||
<Input
|
||||
id="goalDescription"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
placeholder="By end of Q1"
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit" disabled={createMutation.isPending}>
|
||||
{createMutation.isPending ? "Creating..." : "Create Goal"}
|
||||
</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>My Goals</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{isLoading && <p>Loading...</p>}
|
||||
{goals && goals.length === 0 && (
|
||||
<p className="text-muted-foreground">No goals yet</p>
|
||||
)}
|
||||
<div className="space-y-3">
|
||||
{goals?.map((goal) => (
|
||||
<div key={goal.id} className="border rounded-lg p-4">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<h3 className="font-semibold">{goal.name}</h3>
|
||||
<p className="text-lg text-primary">{goal.value}</p>
|
||||
{goal.description && (
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{goal.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
goal.id && deleteMutation.mutate({ id: goal.id })
|
||||
}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Strength Workouts Component
|
||||
// ============================================================================
|
||||
|
||||
function StrengthWorkouts() {
|
||||
const [exercise, setExercise] = useState("Push-ups");
|
||||
const [reps, setReps] = useState("");
|
||||
const [weight, setWeight] = useState("");
|
||||
const [date, setDate] = useState(new Date().toISOString().split("T")[0]);
|
||||
|
||||
const {
|
||||
data: workouts,
|
||||
isLoading,
|
||||
refetch,
|
||||
} = trpc.strength.getAll.useQuery();
|
||||
|
||||
const createMutation = trpc.strength.create.useMutation({
|
||||
onSuccess: () => {
|
||||
refetch();
|
||||
setReps("");
|
||||
setWeight("");
|
||||
},
|
||||
});
|
||||
|
||||
const deleteMutation = trpc.strength.delete.useMutation({
|
||||
onSuccess: () => refetch(),
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
createMutation.mutate({
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
exercise: exercise as any,
|
||||
reps: parseInt(reps),
|
||||
weight: parseFloat(weight),
|
||||
date,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid gap-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Log Strength Workout</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="exercise">Exercise</Label>
|
||||
<select
|
||||
id="exercise"
|
||||
className="w-full p-2 border rounded-md"
|
||||
value={exercise}
|
||||
onChange={(e) => setExercise(e.target.value)}
|
||||
>
|
||||
<option>Push-ups</option>
|
||||
<option>Pull-ups</option>
|
||||
<option>Squats</option>
|
||||
<option>Bench Press</option>
|
||||
<option>Deadlifts</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="workoutDate">Date</Label>
|
||||
<Input
|
||||
id="workoutDate"
|
||||
type="date"
|
||||
value={date}
|
||||
onChange={(e) => setDate(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="reps">Reps</Label>
|
||||
<Input
|
||||
id="reps"
|
||||
type="number"
|
||||
value={reps}
|
||||
onChange={(e) => setReps(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="workoutWeight">Weight (kg)</Label>
|
||||
<Input
|
||||
id="workoutWeight"
|
||||
type="number"
|
||||
step="0.5"
|
||||
value={weight}
|
||||
onChange={(e) => setWeight(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Button type="submit" disabled={createMutation.isPending}>
|
||||
Log Workout
|
||||
</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Workout History</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{isLoading && <p>Loading...</p>}
|
||||
<div className="space-y-2">
|
||||
{workouts?.map((w) => (
|
||||
<div
|
||||
key={w.id}
|
||||
className="flex items-center justify-between p-3 border rounded-lg"
|
||||
>
|
||||
<div>
|
||||
<p className="font-medium">{w.exercise?.[0] || "N/A"}</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{w.reps} reps × {w.weight}kg • {w.date}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => w.id && deleteMutation.mutate({ id: w.id })}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Cardio Exercises Component
|
||||
// ============================================================================
|
||||
|
||||
function CardioExercises() {
|
||||
const [exercise, setExercise] = useState("Running");
|
||||
const [duration, setDuration] = useState("");
|
||||
const [distance, setDistance] = useState("");
|
||||
const [date, setDate] = useState(new Date().toISOString().split("T")[0]);
|
||||
|
||||
const { data: exercises, isLoading, refetch } = trpc.cardio.getAll.useQuery();
|
||||
|
||||
const createMutation = trpc.cardio.create.useMutation({
|
||||
onSuccess: () => {
|
||||
refetch();
|
||||
setDuration("");
|
||||
setDistance("");
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
createMutation.mutate({
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
exercise: exercise as any,
|
||||
duration: parseFloat(duration),
|
||||
distance: parseFloat(distance),
|
||||
date,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid gap-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Log Cardio</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label>Exercise</Label>
|
||||
<select
|
||||
className="w-full p-2 border rounded-md"
|
||||
value={exercise}
|
||||
onChange={(e) => setExercise(e.target.value)}
|
||||
>
|
||||
<option>Running</option>
|
||||
<option>Cycling</option>
|
||||
<option>Swimming</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Date</Label>
|
||||
<Input
|
||||
type="date"
|
||||
value={date}
|
||||
onChange={(e) => setDate(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label>Duration (min)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={duration}
|
||||
onChange={(e) => setDuration(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Distance (km)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
step="0.1"
|
||||
value={distance}
|
||||
onChange={(e) => setDistance(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Button type="submit">Log Exercise</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Cardio History</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{isLoading && <p>Loading...</p>}
|
||||
<div className="space-y-2">
|
||||
{exercises?.map((e) => (
|
||||
<div key={e.id} className="p-3 border rounded-lg">
|
||||
<p className="font-medium">{e.exercise?.[0] || "N/A"}</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{e.distance}km in {e.duration}min • {e.speed?.toFixed(1)} km/h
|
||||
• {e.date}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Calories Tracker Component
|
||||
// ============================================================================
|
||||
|
||||
function CaloriesTracker() {
|
||||
const [date] = useState(new Date().toISOString().split("T")[0]);
|
||||
|
||||
const { data: totals } = trpc.calories.getTotalForDate.useQuery({ date });
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Daily Nutrition</CardTitle>
|
||||
<CardDescription>{date}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{totals && (
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<div className="text-center p-4 border rounded-lg">
|
||||
<p className="text-3xl font-bold">{totals.totalCalories}</p>
|
||||
<p className="text-sm text-muted-foreground">Calories</p>
|
||||
</div>
|
||||
<div className="text-center p-4 border rounded-lg">
|
||||
<p className="text-3xl font-bold">{totals.totalProtein}g</p>
|
||||
<p className="text-sm text-muted-foreground">Protein</p>
|
||||
</div>
|
||||
<div className="text-center p-4 border rounded-lg">
|
||||
<p className="text-3xl font-bold">{totals.totalCarbs}g</p>
|
||||
<p className="text-sm text-muted-foreground">Carbs</p>
|
||||
</div>
|
||||
<div className="text-center p-4 border rounded-lg">
|
||||
<p className="text-3xl font-bold">{totals.totalFats}g</p>
|
||||
<p className="text-sm text-muted-foreground">Fats</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
9
apps/client/tsconfig.json
Normal file
9
apps/client/tsconfig.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"extends": "./tsconfig.app.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"include": ["src", "vite.config.ts"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
3
apps/server/.env.example
Normal file
3
apps/server/.env.example
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
TAYLORDB_BASE_ID=
|
||||
TAYLORDB_BASE_URL=
|
||||
TAYLORDB_API_KEY=
|
||||
50
apps/server/index.ts
Normal file
50
apps/server/index.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import express from "express";
|
||||
import cors from "cors";
|
||||
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;
|
||||
|
||||
// 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("/health", (_req, res) => {
|
||||
res.json({ status: "ok", timestamp: new Date().toISOString() });
|
||||
});
|
||||
|
||||
// tRPC middleware
|
||||
app.use(
|
||||
"/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}/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}/trpc`);
|
||||
});
|
||||
31
apps/server/package.json
Normal file
31
apps/server/package.json
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"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 --env-file=.env index.ts",
|
||||
"build": "tsc",
|
||||
"start": "node dist/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@taylordb/query-builder": "^0.10.3",
|
||||
"@trpc/server": "^11.8.1",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^5.2.1",
|
||||
"zod": "^4.3.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cors": "^2.8.19",
|
||||
"@types/express": "^5.0.6",
|
||||
"@types/node": "^24.10.1",
|
||||
"concurrently": "^9.2.1",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "~5.9.3"
|
||||
}
|
||||
}
|
||||
354
apps/server/router.ts
Normal file
354
apps/server/router.ts
Normal file
|
|
@ -0,0 +1,354 @@
|
|||
import { z } from "zod";
|
||||
import { router, publicProcedure } from "./trpc";
|
||||
import * as db from "./taylordb/query-builder";
|
||||
import {
|
||||
StrengthExerciseOptions,
|
||||
CaloriesTimeOfDayOptions,
|
||||
CardioExerciseOptions,
|
||||
CaloriesUnitOptions,
|
||||
} from "./taylordb/types";
|
||||
|
||||
/**
|
||||
* Main tRPC router with TaylorDB integration
|
||||
* All procedures are type-safe and use the TaylorDB query builder
|
||||
*/
|
||||
export const appRouter = router({
|
||||
// ============================================================================
|
||||
// Example / Test Procedures
|
||||
// ============================================================================
|
||||
|
||||
hello: publicProcedure
|
||||
.input(z.object({ name: z.string().optional() }).optional())
|
||||
.query(({ input }) => {
|
||||
return {
|
||||
message: `Hello ${input?.name || "from tRPC"}!`,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
}),
|
||||
|
||||
health: publicProcedure.query(() => {
|
||||
return {
|
||||
status: "ok",
|
||||
timestamp: new Date().toISOString(),
|
||||
uptime: process.uptime(),
|
||||
};
|
||||
}),
|
||||
|
||||
// ============================================================================
|
||||
// Weight Tracking
|
||||
// ============================================================================
|
||||
|
||||
weight: {
|
||||
getAll: publicProcedure.query(async () => {
|
||||
return await db.getAllWeightRecords();
|
||||
}),
|
||||
|
||||
getById: publicProcedure
|
||||
.input(z.object({ id: z.number() }))
|
||||
.query(async ({ input }) => {
|
||||
return await db.getWeightRecordById(input.id);
|
||||
}),
|
||||
|
||||
getByDateRange: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
startDate: z.string(),
|
||||
endDate: z.string(),
|
||||
})
|
||||
)
|
||||
.query(async ({ input }) => {
|
||||
return await db.getWeightRecordsByDateRange(
|
||||
input.startDate,
|
||||
input.endDate
|
||||
);
|
||||
}),
|
||||
|
||||
create: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
date: z.string(),
|
||||
weight: z.number().positive(),
|
||||
name: z.string().optional(),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
return await db.createWeightRecord(input);
|
||||
}),
|
||||
|
||||
update: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.number(),
|
||||
weight: z.number().positive().optional(),
|
||||
date: z.string().optional(),
|
||||
name: z.string().optional(),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
const { id, ...data } = input;
|
||||
return await db.updateWeightRecord(id, data);
|
||||
}),
|
||||
|
||||
delete: publicProcedure
|
||||
.input(z.object({ id: z.number() }))
|
||||
.mutation(async ({ input }) => {
|
||||
return await db.deleteWeightRecord(input.id);
|
||||
}),
|
||||
|
||||
deleteMultiple: publicProcedure
|
||||
.input(z.object({ ids: z.array(z.number()) }))
|
||||
.mutation(async ({ input }) => {
|
||||
return await db.deleteWeightRecords(input.ids);
|
||||
}),
|
||||
|
||||
getStats: publicProcedure.query(async () => {
|
||||
return await db.getWeightStats();
|
||||
}),
|
||||
},
|
||||
|
||||
// ============================================================================
|
||||
// Goals Management
|
||||
// ============================================================================
|
||||
|
||||
goals: {
|
||||
getAll: publicProcedure.query(async () => {
|
||||
return await db.getAllGoals();
|
||||
}),
|
||||
|
||||
create: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
name: z.string().min(1),
|
||||
value: z.string().min(1),
|
||||
description: z.string().optional(),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
return await db.createGoal(input);
|
||||
}),
|
||||
|
||||
update: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.number(),
|
||||
name: z.string().optional(),
|
||||
value: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
const { id, ...data } = input;
|
||||
return await db.updateGoal(id, data);
|
||||
}),
|
||||
|
||||
delete: publicProcedure
|
||||
.input(z.object({ id: z.number() }))
|
||||
.mutation(async ({ input }) => {
|
||||
return await db.deleteGoal(input.id);
|
||||
}),
|
||||
},
|
||||
|
||||
// ============================================================================
|
||||
// Strength Training
|
||||
// ============================================================================
|
||||
|
||||
strength: {
|
||||
getAll: publicProcedure
|
||||
.input(
|
||||
z
|
||||
.object({ exercise: z.enum(StrengthExerciseOptions).optional() })
|
||||
.optional()
|
||||
)
|
||||
.query(async ({ input }) => {
|
||||
return await db.getStrengthWorkouts(input?.exercise);
|
||||
}),
|
||||
|
||||
create: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
exercise: z.enum(StrengthExerciseOptions),
|
||||
reps: z.number().positive(),
|
||||
weight: z.number().positive(),
|
||||
date: z.string(),
|
||||
name: z.string().optional(),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
return await db.createStrengthWorkout(input);
|
||||
}),
|
||||
|
||||
update: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.number(),
|
||||
exercise: z.enum(StrengthExerciseOptions).optional(),
|
||||
reps: z.number().positive().optional(),
|
||||
weight: z.number().positive().optional(),
|
||||
date: z.string().optional(),
|
||||
name: z.string().optional(),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
const { id, ...data } = input;
|
||||
return await db.updateStrengthWorkout(id, data);
|
||||
}),
|
||||
|
||||
delete: publicProcedure
|
||||
.input(z.object({ id: z.number() }))
|
||||
.mutation(async ({ input }) => {
|
||||
return await db.deleteStrengthWorkout(input.id);
|
||||
}),
|
||||
},
|
||||
|
||||
// ============================================================================
|
||||
// Cardio Exercise
|
||||
// ============================================================================
|
||||
|
||||
cardio: {
|
||||
getAll: publicProcedure.query(async () => {
|
||||
return await db.getCardioExercises();
|
||||
}),
|
||||
|
||||
create: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
exercise: z.enum(CardioExerciseOptions),
|
||||
duration: z.number().positive(),
|
||||
distance: z.number().positive(),
|
||||
date: z.string(),
|
||||
name: z.string().optional(),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
return await db.createCardioExercise(input);
|
||||
}),
|
||||
|
||||
update: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.number(),
|
||||
exercise: z.enum(CardioExerciseOptions).optional(),
|
||||
duration: z.number().positive().optional(),
|
||||
distance: z.number().positive().optional(),
|
||||
date: z.string().optional(),
|
||||
name: z.string().optional(),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
const { id, ...data } = input;
|
||||
return await db.updateCardioExercise(id, data);
|
||||
}),
|
||||
|
||||
delete: publicProcedure
|
||||
.input(z.object({ id: z.number() }))
|
||||
.mutation(async ({ input }) => {
|
||||
return await db.deleteCardioExercise(input.id);
|
||||
}),
|
||||
},
|
||||
|
||||
// ============================================================================
|
||||
// Calories/Nutrition Tracking
|
||||
// ============================================================================
|
||||
|
||||
calories: {
|
||||
getByTimeOfDay: publicProcedure
|
||||
.input(z.object({ timeOfDay: z.enum(CaloriesTimeOfDayOptions) }))
|
||||
.query(async ({ input }) => {
|
||||
return await db.getCaloriesByTimeOfDay(input.timeOfDay);
|
||||
}),
|
||||
|
||||
create: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
date: z.string(),
|
||||
timeOfDay: z.enum(CaloriesTimeOfDayOptions),
|
||||
mealName: z.string(),
|
||||
mealIngredient: z.string(),
|
||||
quantity: z.number().positive(),
|
||||
unit: z.enum(CaloriesUnitOptions),
|
||||
totalCalories: z.number(),
|
||||
totalProtein: z.number(),
|
||||
totalCarbs: z.number(),
|
||||
totalFats: z.number(),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
return await db.createCalorieEntry(input);
|
||||
}),
|
||||
|
||||
update: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.number(),
|
||||
totalCalories: z.number().optional(),
|
||||
totalProtein: z.number().optional(),
|
||||
totalCarbs: z.number().optional(),
|
||||
totalFats: z.number().optional(),
|
||||
quantity: z.number().positive().optional(),
|
||||
mealName: z.string().optional(),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
const { id, ...data } = input;
|
||||
return await db.updateCalorieEntry(id, data);
|
||||
}),
|
||||
|
||||
delete: publicProcedure
|
||||
.input(z.object({ id: z.number() }))
|
||||
.mutation(async ({ input }) => {
|
||||
return await db.deleteCalorieEntry(input.id);
|
||||
}),
|
||||
|
||||
getTotalForDate: publicProcedure
|
||||
.input(z.object({ date: z.string() }))
|
||||
.query(async ({ input }) => {
|
||||
return await db.getTotalCaloriesForDate(input.date);
|
||||
}),
|
||||
},
|
||||
|
||||
// ============================================================================
|
||||
// Settings
|
||||
// ============================================================================
|
||||
|
||||
settings: {
|
||||
getByName: publicProcedure
|
||||
.input(z.object({ name: z.string() }))
|
||||
.query(async ({ input }) => {
|
||||
return await db.getSettingByName(input.name);
|
||||
}),
|
||||
|
||||
create: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
name: z.string().min(1),
|
||||
value: z.string(),
|
||||
description: z.string().optional(),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
return await db.createSetting(input);
|
||||
}),
|
||||
|
||||
update: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.number(),
|
||||
value: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
const { id, ...data } = input;
|
||||
return await db.updateSetting(id, data);
|
||||
}),
|
||||
|
||||
delete: publicProcedure
|
||||
.input(z.object({ id: z.number() }))
|
||||
.mutation(async ({ input }) => {
|
||||
return await db.deleteSetting(input.id);
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
// Export type definition of API
|
||||
export type AppRouter = typeof appRouter;
|
||||
534
apps/server/taylordb/query-builder.ts
Normal file
534
apps/server/taylordb/query-builder.ts
Normal file
|
|
@ -0,0 +1,534 @@
|
|||
import pkg from "@taylordb/query-builder";
|
||||
const { createQueryBuilder } = pkg;
|
||||
import type {
|
||||
StrengthExerciseOptions,
|
||||
CaloriesTimeOfDayOptions,
|
||||
CardioExerciseOptions,
|
||||
CaloriesUnitOptions,
|
||||
} from "./types.js";
|
||||
import type { TaylorDatabase } from "./types.js";
|
||||
|
||||
export const queryBuilder = createQueryBuilder<TaylorDatabase>({
|
||||
baseUrl: process.env.TAYLORDB_BASE_URL!,
|
||||
baseId: process.env.TAYLORDB_BASE_ID!,
|
||||
apiKey: process.env.TAYLORDB_API_KEY!,
|
||||
});
|
||||
|
||||
/**
|
||||
* Sample CRUD Operations for TaylorDB
|
||||
* These demonstrate how to interact with the database using the query builder
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// READ Operations (Queries)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Get all weight records
|
||||
*/
|
||||
export async function getAllWeightRecords() {
|
||||
return await queryBuilder
|
||||
.selectFrom("weight")
|
||||
.select(["id", "date", "weight", "name", "createdAt", "updatedAt"])
|
||||
.orderBy("date", "desc")
|
||||
.execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get weight records for a specific date range
|
||||
*/
|
||||
export async function getWeightRecordsByDateRange(
|
||||
startDate: string,
|
||||
endDate: string
|
||||
) {
|
||||
return await queryBuilder
|
||||
.selectFrom("weight")
|
||||
.where("date", ">=", ["exactDay", startDate])
|
||||
.where("date", "<=", ["exactDay", endDate])
|
||||
.orderBy("date", "asc")
|
||||
.execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single weight record by ID
|
||||
*/
|
||||
export async function getWeightRecordById(id: number) {
|
||||
return await queryBuilder
|
||||
.selectFrom("weight")
|
||||
.where("id", "=", id)
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all goals
|
||||
*/
|
||||
export async function getAllGoals() {
|
||||
return await queryBuilder
|
||||
.selectFrom("goals")
|
||||
.select(["id", "name", "value", "description", "createdAt", "updatedAt"])
|
||||
.execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get strength workout records with optional filtering by exercise
|
||||
*/
|
||||
export async function getStrengthWorkouts(
|
||||
exercise?: (typeof StrengthExerciseOptions)[number]
|
||||
) {
|
||||
let query = queryBuilder
|
||||
.selectFrom("strength")
|
||||
.select(["id", "date", "exercise", "reps", "weight", "name"])
|
||||
.orderBy("date", "desc");
|
||||
|
||||
if (exercise) {
|
||||
query = query.where("exercise", "=", exercise);
|
||||
}
|
||||
|
||||
return await query.execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cardio exercises
|
||||
*/
|
||||
export async function getCardioExercises() {
|
||||
return await queryBuilder
|
||||
.selectFrom("cardio")
|
||||
.select(["id", "date", "exercise", "duration", "distance", "speed", "name"])
|
||||
.orderBy("date", "desc")
|
||||
.execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get calories by time of day
|
||||
*/
|
||||
export async function getCaloriesByTimeOfDay(
|
||||
timeOfDay: (typeof CaloriesTimeOfDayOptions)[number]
|
||||
) {
|
||||
return await queryBuilder
|
||||
.selectFrom("calories")
|
||||
.select([
|
||||
"id",
|
||||
"date",
|
||||
"timeOfDay",
|
||||
"mealName",
|
||||
"totalCalories",
|
||||
"totalProtein",
|
||||
"totalCarbs",
|
||||
"totalFats",
|
||||
])
|
||||
.where("timeOfDay", "=", timeOfDay)
|
||||
.execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get settings by name
|
||||
*/
|
||||
export async function getSettingByName(name: string) {
|
||||
return await queryBuilder
|
||||
.selectFrom("settings")
|
||||
.where("name", "=", name)
|
||||
.execute();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CREATE Operations (Insert)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Add a new weight record
|
||||
*/
|
||||
export async function createWeightRecord(data: {
|
||||
date: string;
|
||||
weight: number;
|
||||
name?: string;
|
||||
}) {
|
||||
return await queryBuilder
|
||||
.insertInto("weight")
|
||||
.values({
|
||||
date: data.date,
|
||||
weight: data.weight,
|
||||
name: data.name || "",
|
||||
})
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new goal
|
||||
*/
|
||||
export async function createGoal(data: {
|
||||
name: string;
|
||||
value: string;
|
||||
description?: string;
|
||||
}) {
|
||||
return await queryBuilder
|
||||
.insertInto("goals")
|
||||
.values({
|
||||
name: data.name,
|
||||
value: data.value,
|
||||
description: data.description || "",
|
||||
})
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a strength workout
|
||||
*/
|
||||
export async function createStrengthWorkout(data: {
|
||||
exercise: (typeof StrengthExerciseOptions)[number];
|
||||
reps: number;
|
||||
weight: number;
|
||||
date: string;
|
||||
name?: string;
|
||||
}) {
|
||||
return await queryBuilder
|
||||
.insertInto("strength")
|
||||
.values({
|
||||
exercise: [data.exercise],
|
||||
reps: data.reps,
|
||||
weight: data.weight,
|
||||
date: data.date,
|
||||
name: data.name || "",
|
||||
})
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a cardio exercise
|
||||
*/
|
||||
export async function createCardioExercise(data: {
|
||||
exercise: (typeof CardioExerciseOptions)[number];
|
||||
duration: number;
|
||||
distance: number;
|
||||
date: string;
|
||||
name?: string;
|
||||
}) {
|
||||
const speed = data.distance / (data.duration / 60); // Calculate speed (km/h)
|
||||
|
||||
return await queryBuilder
|
||||
.insertInto("cardio")
|
||||
.values({
|
||||
exercise: [data.exercise],
|
||||
duration: data.duration,
|
||||
distance: data.distance,
|
||||
speed,
|
||||
date: data.date,
|
||||
name: data.name || "",
|
||||
})
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
/**
|
||||
* Log calories/meal
|
||||
*/
|
||||
export async function createCalorieEntry(data: {
|
||||
date: string;
|
||||
timeOfDay: (typeof CaloriesTimeOfDayOptions)[number];
|
||||
mealName: string;
|
||||
mealIngredient: string;
|
||||
quantity: number;
|
||||
unit: (typeof CaloriesUnitOptions)[number];
|
||||
totalCalories: number;
|
||||
totalProtein: number;
|
||||
totalCarbs: number;
|
||||
totalFats: number;
|
||||
}) {
|
||||
return await queryBuilder
|
||||
.insertInto("calories")
|
||||
.values({
|
||||
date: data.date,
|
||||
timeOfDay: [data.timeOfDay],
|
||||
mealName: data.mealName,
|
||||
mealIngredient: data.mealIngredient,
|
||||
quantity: data.quantity,
|
||||
unit: [data.unit],
|
||||
totalCalories: data.totalCalories,
|
||||
totalProtein: data.totalProtein,
|
||||
totalCarbs: data.totalCarbs,
|
||||
totalFats: data.totalFats,
|
||||
name: "",
|
||||
proteinPer100G: 0,
|
||||
carbsPer100G: 0,
|
||||
fatsPer100G: 0,
|
||||
quantityInGramsmL: 0,
|
||||
quantityInFlOzozlb: 0,
|
||||
})
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create or update a setting
|
||||
*/
|
||||
export async function createSetting(data: {
|
||||
name: string;
|
||||
value: string;
|
||||
description?: string;
|
||||
}) {
|
||||
return await queryBuilder
|
||||
.insertInto("settings")
|
||||
.values({
|
||||
name: data.name,
|
||||
value: data.value,
|
||||
description: data.description || "",
|
||||
})
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// UPDATE Operations
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Update a weight record
|
||||
*/
|
||||
export async function updateWeightRecord(
|
||||
id: number,
|
||||
data: {
|
||||
weight?: number;
|
||||
date?: string;
|
||||
name?: string;
|
||||
}
|
||||
) {
|
||||
return await queryBuilder
|
||||
.update("weight")
|
||||
.set(data)
|
||||
.where("id", "=", id)
|
||||
.execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a goal
|
||||
*/
|
||||
export async function updateGoal(
|
||||
id: number,
|
||||
data: {
|
||||
name?: string;
|
||||
value?: string;
|
||||
description?: string;
|
||||
}
|
||||
) {
|
||||
return await queryBuilder
|
||||
.update("goals")
|
||||
.set(data)
|
||||
.where("id", "=", id)
|
||||
.execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a strength workout
|
||||
*/
|
||||
export async function updateStrengthWorkout(
|
||||
id: number,
|
||||
data: {
|
||||
reps?: number;
|
||||
weight?: number;
|
||||
exercise?: (typeof StrengthExerciseOptions)[number];
|
||||
date?: string;
|
||||
name?: string;
|
||||
}
|
||||
) {
|
||||
const updateData: Record<string, string | number | string[] | undefined> = {
|
||||
...data,
|
||||
exercise: data.exercise ? [data.exercise] : undefined,
|
||||
};
|
||||
return await queryBuilder
|
||||
.update("strength")
|
||||
.set(updateData)
|
||||
.where("id", "=", id)
|
||||
.execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a cardio exercise
|
||||
*/
|
||||
export async function updateCardioExercise(
|
||||
id: number,
|
||||
data: {
|
||||
duration?: number;
|
||||
distance?: number;
|
||||
exercise?: (typeof CardioExerciseOptions)[number];
|
||||
date?: string;
|
||||
name?: string;
|
||||
}
|
||||
) {
|
||||
const updateData: Record<string, string | number | string[] | undefined> = {
|
||||
...data,
|
||||
exercise: data.exercise ? [data.exercise] : undefined,
|
||||
};
|
||||
|
||||
// Recalculate speed if duration or distance changed
|
||||
if (data.duration || data.distance) {
|
||||
const record = await queryBuilder
|
||||
.selectFrom("cardio")
|
||||
.select(["duration", "distance"])
|
||||
.where("id", "=", id)
|
||||
.executeTakeFirst();
|
||||
|
||||
if (record) {
|
||||
const newDuration = data.duration ?? record.duration ?? 0;
|
||||
const newDistance = data.distance ?? record.distance ?? 0;
|
||||
updateData.speed = newDistance / (newDuration / 60);
|
||||
}
|
||||
}
|
||||
|
||||
return await queryBuilder
|
||||
.update("cardio")
|
||||
.set(updateData)
|
||||
.where("id", "=", id)
|
||||
.execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a calorie entry
|
||||
*/
|
||||
export async function updateCalorieEntry(
|
||||
id: number,
|
||||
data: {
|
||||
totalCalories?: number;
|
||||
totalProtein?: number;
|
||||
totalCarbs?: number;
|
||||
totalFats?: number;
|
||||
quantity?: number;
|
||||
mealName?: string;
|
||||
}
|
||||
) {
|
||||
return await queryBuilder
|
||||
.update("calories")
|
||||
.set(data)
|
||||
.where("id", "=", id)
|
||||
.execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a setting
|
||||
*/
|
||||
export async function updateSetting(
|
||||
id: number,
|
||||
data: {
|
||||
value?: string;
|
||||
description?: string;
|
||||
}
|
||||
) {
|
||||
return await queryBuilder
|
||||
.update("settings")
|
||||
.set(data)
|
||||
.where("id", "=", id)
|
||||
.execute();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// DELETE Operations
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Delete a weight record
|
||||
*/
|
||||
export async function deleteWeightRecord(id: number) {
|
||||
return await queryBuilder.deleteFrom("weight").where("id", "=", id).execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete multiple weight records by IDs
|
||||
*/
|
||||
export async function deleteWeightRecords(ids: number[]) {
|
||||
return await queryBuilder
|
||||
.deleteFrom("weight")
|
||||
.where("id", "hasAnyOf", ids)
|
||||
.execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a goal
|
||||
*/
|
||||
export async function deleteGoal(id: number) {
|
||||
return await queryBuilder.deleteFrom("goals").where("id", "=", id).execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a strength workout
|
||||
*/
|
||||
export async function deleteStrengthWorkout(id: number) {
|
||||
return await queryBuilder
|
||||
.deleteFrom("strength")
|
||||
.where("id", "=", id)
|
||||
.execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a cardio exercise
|
||||
*/
|
||||
export async function deleteCardioExercise(id: number) {
|
||||
return await queryBuilder.deleteFrom("cardio").where("id", "=", id).execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a calorie entry
|
||||
*/
|
||||
export async function deleteCalorieEntry(id: number) {
|
||||
return await queryBuilder
|
||||
.deleteFrom("calories")
|
||||
.where("id", "=", id)
|
||||
.execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a setting
|
||||
*/
|
||||
export async function deleteSetting(id: number) {
|
||||
return await queryBuilder
|
||||
.deleteFrom("settings")
|
||||
.where("id", "=", id)
|
||||
.execute();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// AGGREGATION Operations (Advanced)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Get weight statistics using aggregation
|
||||
*/
|
||||
export async function getWeightStats() {
|
||||
// Note: TaylorDB aggregation API may differ, this is a placeholder
|
||||
// Use the aggregateFrom method if available
|
||||
const weights = await queryBuilder
|
||||
.selectFrom("weight")
|
||||
.select(["weight"])
|
||||
.execute();
|
||||
|
||||
if (weights.length === 0) {
|
||||
return {
|
||||
count: 0,
|
||||
average: null,
|
||||
min: null,
|
||||
max: null,
|
||||
};
|
||||
}
|
||||
|
||||
const values = weights
|
||||
.map((w) => w.weight)
|
||||
.filter((w): w is number => w !== undefined);
|
||||
return {
|
||||
count: values.length,
|
||||
average: values.reduce((a, b) => a + b, 0) / values.length,
|
||||
min: Math.min(...values),
|
||||
max: Math.max(...values),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total calories for a date
|
||||
*/
|
||||
export async function getTotalCaloriesForDate(date: string) {
|
||||
const entries = await queryBuilder
|
||||
.selectFrom("calories")
|
||||
.select(["totalCalories", "totalProtein", "totalCarbs", "totalFats"])
|
||||
.where("date", "=", ["exactDay", date])
|
||||
.execute();
|
||||
|
||||
return {
|
||||
totalCalories: entries.reduce((sum, e) => sum + (e.totalCalories ?? 0), 0),
|
||||
totalProtein: entries.reduce((sum, e) => sum + (e.totalProtein ?? 0), 0),
|
||||
totalCarbs: entries.reduce((sum, e) => sum + (e.totalCarbs ?? 0), 0),
|
||||
totalFats: entries.reduce((sum, e) => sum + (e.totalFats ?? 0), 0),
|
||||
};
|
||||
}
|
||||
465
apps/server/taylordb/types.ts
Normal file
465
apps/server/taylordb/types.ts
Normal 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>;
|
||||
};
|
||||
27
apps/server/trpc.ts
Normal file
27
apps/server/trpc.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { initTRPC } from "@trpc/server";
|
||||
import type { CreateExpressContextOptions } from "@trpc/server/adapters/express";
|
||||
|
||||
/**
|
||||
* Create context for each tRPC request
|
||||
* This is where you can add user session, database clients, etc.
|
||||
*/
|
||||
export const createContext = ({ req, res }: CreateExpressContextOptions) => {
|
||||
return {
|
||||
req,
|
||||
res,
|
||||
// Add any shared context here (e.g., database client, user session)
|
||||
};
|
||||
};
|
||||
|
||||
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
20
apps/server/tsconfig.json
Normal 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"]
|
||||
}
|
||||
49
package.json
49
package.json
|
|
@ -1,47 +1,20 @@
|
|||
{
|
||||
"name": "blank",
|
||||
"name": "taylordb-monorepo",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"version": "0.0.10",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview",
|
||||
"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"
|
||||
},
|
||||
"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",
|
||||
"@taylordb/query-builder": "^0.10.1",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.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"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@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",
|
||||
"concurrently": "^9.2.1",
|
||||
"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"
|
||||
"typescript": "~5.9.3"
|
||||
}
|
||||
}
|
||||
1263
pnpm-lock.yaml
1263
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
2
pnpm-workspace.yaml
Normal file
2
pnpm-workspace.yaml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
packages:
|
||||
- "apps/*"
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
import { createQueryBuilder } from "@taylordb/query-builder";
|
||||
import type { TaylorDatabase } from "./taylordb.types";
|
||||
|
||||
let apiKey: string | undefined;
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
const apiKeyFromParams = searchParams.get("apiKey");
|
||||
|
||||
if (apiKeyFromParams) {
|
||||
// Store in session storage if found in search params
|
||||
sessionStorage.setItem("authToken", apiKeyFromParams);
|
||||
apiKey = apiKeyFromParams;
|
||||
} else {
|
||||
// If not in search params, try to get it from session storage
|
||||
apiKey = sessionStorage.getItem("authToken") ?? undefined;
|
||||
}
|
||||
}
|
||||
|
||||
if (!apiKey) {
|
||||
throw new Error("No authentication token found");
|
||||
}
|
||||
|
||||
export const queryBuilder = createQueryBuilder<TaylorDatabase>({
|
||||
baseUrl: import.meta.env.VITE_TAYLORDB_BASE_URL,
|
||||
baseId: import.meta.env.VITE_TAYLORDB_BASE_ID,
|
||||
apiKey,
|
||||
});
|
||||
|
|
@ -1,7 +1,4 @@
|
|||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
"references": [{ "path": "./apps/client" }, { "path": "./apps/server" }]
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user