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
|
node_modules
|
||||||
dist
|
dist
|
||||||
dist-ssr
|
dist-ssr
|
||||||
|
server/dist
|
||||||
*.local
|
*.local
|
||||||
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
|
|
@ -24,6 +25,7 @@ dist-ssr
|
||||||
*.sw?
|
*.sw?
|
||||||
|
|
||||||
.env*
|
.env*
|
||||||
|
!.env.example
|
||||||
.aider*
|
.aider*
|
||||||
|
|
||||||
src/lib/taylordb.types.ts
|
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
|
## 📦 Monorepo Structure
|
||||||
- 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`
|
taylordb-clientserver-template/
|
||||||
- Path aliases via `@` (`@/components`, `@/lib`, etc.)
|
├── 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
|
```bash
|
||||||
|
# Install all workspace dependencies
|
||||||
pnpm install
|
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
|
Visit:
|
||||||
The project is already initialized with `components.json`. Add more components with:
|
|
||||||
|
- **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
|
```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
|
```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
|
## 🛠️ Available Scripts
|
||||||
- Layout/structure: `card`, `tabs`
|
|
||||||
- Form controls: `input`, `label`, `textarea`, `select`
|
|
||||||
- Feedback: `alert`
|
|
||||||
- Buttons: `button`
|
|
||||||
|
|
||||||
## TaylorDB integration
|
### Root Commands (run from anywhere)
|
||||||
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.
|
|
||||||
|
|
||||||
## Scripts
|
```bash
|
||||||
- `pnpm dev` — run Vite dev server (HMR)
|
# Development
|
||||||
- `pnpm build` — type-check + build
|
pnpm dev:full # Run both workspaces concurrently
|
||||||
- `pnpm lint` — ESLint (strict, no `any`)
|
pnpm dev # Frontend only
|
||||||
|
pnpm dev:server # Backend only
|
||||||
|
|
||||||
## Notes
|
# Building
|
||||||
- Tailwind 4 uses `@tailwindcss/vite`; CSS entry is `@import "tailwindcss";` in `src/index.css`.
|
pnpm build # Build frontend
|
||||||
- Tailwind config is in `tailwind.config.js`; CSS tokens live in `src/index.css`.
|
pnpm build:server # Build backend
|
||||||
- Components use `@/lib/utils` for the `cn` helper (clsx + tailwind-merge).
|
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 = [
|
const navItems = [
|
||||||
{ to: "/", label: "Home" },
|
{ to: "/", label: "Home" },
|
||||||
{ to: "/about", label: "About" },
|
{ to: "/about", label: "About" },
|
||||||
|
{ to: "/trpc-demo", label: "tRPC Demo" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const getInitialTheme = (): "light" | "dark" => {
|
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 { StrictMode } from "react";
|
||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from "react-dom/client";
|
||||||
import { createBrowserRouter, RouterProvider } from "react-router-dom";
|
import { createBrowserRouter, RouterProvider } from "react-router-dom";
|
||||||
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
|
|
||||||
import App from "./App";
|
import App from "./App";
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
import AboutPage from "./pages/AboutPage";
|
import AboutPage from "./pages/AboutPage";
|
||||||
import HomePage from "./pages/HomePage";
|
import HomePage from "./pages/HomePage";
|
||||||
import NotFoundPage from "./pages/NotFoundPage";
|
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([
|
const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
|
|
@ -15,6 +27,7 @@ const router = createBrowserRouter([
|
||||||
children: [
|
children: [
|
||||||
{ index: true, element: <HomePage /> },
|
{ index: true, element: <HomePage /> },
|
||||||
{ path: "about", element: <AboutPage /> },
|
{ path: "about", element: <AboutPage /> },
|
||||||
|
{ path: "trpc-demo", element: <TRPCDemoPage /> },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{ path: "*", element: <NotFoundPage /> },
|
{ path: "*", element: <NotFoundPage /> },
|
||||||
|
|
@ -22,6 +35,10 @@ const router = createBrowserRouter([
|
||||||
|
|
||||||
createRoot(document.getElementById("root")!).render(
|
createRoot(document.getElementById("root")!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<RouterProvider router={router} />
|
<trpc.Provider client={trpcClient} queryClient={queryClient}>
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<RouterProvider router={router} />
|
||||||
|
</QueryClientProvider>
|
||||||
|
</trpc.Provider>
|
||||||
</StrictMode>
|
</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,
|
"private": true,
|
||||||
"version": "0.0.10",
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "pnpm --filter @repo/client dev",
|
||||||
"build": "tsc -b && vite build",
|
"dev:server": "pnpm --filter @repo/server dev",
|
||||||
"lint": "eslint .",
|
"dev:full": "concurrently \"pnpm dev\" \"pnpm dev:server\" --names \"client,server\" --prefix-colors \"cyan,magenta\"",
|
||||||
"preview": "vite preview",
|
"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"
|
"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": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.1",
|
"concurrently": "^9.2.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": "^9.39.1",
|
||||||
"eslint-plugin-react-hooks": "^7.0.1",
|
"typescript": "~5.9.3"
|
||||||
"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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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": [],
|
"files": [],
|
||||||
"references": [
|
"references": [{ "path": "./apps/client" }, { "path": "./apps/server" }]
|
||||||
{ "path": "./tsconfig.app.json" },
|
|
||||||
{ "path": "./tsconfig.node.json" }
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user