Strapsr Local host
43
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
/app/generated/prisma
|
||||
388
DOKUMENTASI_APP_FOLDER.md
Normal file
|
|
@ -0,0 +1,388 @@
|
|||
# Dokumentasi Struktur Folder `app/`
|
||||
|
||||
## Gambaran Umum
|
||||
|
||||
Folder `app/` adalah direktori utama aplikasi Next.js yang menggunakan App Router. Folder ini berisi semua halaman, API routes, dan komponen utama aplikasi pelatihan kebugaran "Straps".
|
||||
|
||||
---
|
||||
|
||||
## 📁 Struktur Direktori
|
||||
|
||||
### 1. **`api/` - API Routes**
|
||||
|
||||
Folder yang berisi semua endpoint backend untuk komunikasi antara client dan server.
|
||||
|
||||
#### **`api/auth/[...nextauth]/`**
|
||||
|
||||
- **Fungsi**: Menangani autentikasi pengguna menggunakan NextAuth.js
|
||||
- **Endpoint**: `/api/auth/*`
|
||||
- **Fitur**: Login, logout, session management
|
||||
|
||||
#### **`api/clients/`**
|
||||
|
||||
- **Fungsi**: Endpoint untuk mengelola data klien
|
||||
- **Operasi**: CRUD operations untuk client data
|
||||
|
||||
#### **`api/coach/link-client/`**
|
||||
|
||||
- **File**: `route.ts`
|
||||
- **Fungsi**: Menghubungkan pelatih (coach) dengan klien mereka
|
||||
- **Method**: POST untuk membuat hubungan coach-client
|
||||
|
||||
#### **`api/logs/`**
|
||||
|
||||
- **File**: `route.ts`
|
||||
- **Fungsi**: Mencatat dan mengambil log aktivitas training
|
||||
- **Method**: GET (mengambil logs), POST (membuat log baru)
|
||||
|
||||
#### **`api/menus/`**
|
||||
|
||||
- **File**: `route.ts`
|
||||
- **Fungsi**: Mengelola menu latihan (workout programs)
|
||||
- **Method**: GET (semua menu), POST (buat menu baru)
|
||||
|
||||
##### **`api/menus/[id]/`**
|
||||
|
||||
- **File**: `route.ts`
|
||||
- **Fungsi**: Operasi pada menu spesifik berdasarkan ID
|
||||
- **Method**: GET (detail menu), PUT (update), DELETE (hapus)
|
||||
|
||||
#### **`api/recap/`**
|
||||
|
||||
- **File**: `route.ts`
|
||||
- **Fungsi**: Menyimpan dan mengambil ringkasan latihan (training recap)
|
||||
- **Method**: GET (semua recap), POST (simpan recap baru)
|
||||
- **Data**: Termasuk skor form, detail per-rep, dan statistik latihan
|
||||
|
||||
##### **`api/recap/[id]/`**
|
||||
|
||||
- **File**: `route.ts`
|
||||
- **Fungsi**: Mengakses recap latihan spesifik
|
||||
- **Method**: GET (detail recap), DELETE (hapus recap)
|
||||
|
||||
#### **`api/register/`**
|
||||
|
||||
- **File**: `route.ts`
|
||||
- **Fungsi**: Registrasi pengguna baru
|
||||
- **Method**: POST untuk membuat akun baru (Coach atau Client)
|
||||
- **Validasi**: Email unik, password hashing
|
||||
|
||||
#### **`api/status/`**
|
||||
|
||||
- **Fungsi**: Health check endpoint untuk monitoring aplikasi
|
||||
- **Method**: GET untuk cek status server
|
||||
|
||||
#### **`api/users/`**
|
||||
|
||||
- **File**: `route.ts`
|
||||
- **Fungsi**: Mengelola data pengguna
|
||||
- **Method**: GET (daftar users), POST (buat user)
|
||||
|
||||
##### **`api/users/[id]/`**
|
||||
|
||||
- **File**: `route.ts`
|
||||
- **Fungsi**: Operasi pada user spesifik
|
||||
- **Method**: GET (detail user), PUT (update), DELETE (hapus)
|
||||
|
||||
---
|
||||
|
||||
### 2. **`client/` - Halaman Klien**
|
||||
|
||||
Folder untuk semua halaman yang diakses oleh pengguna dengan role "Client".
|
||||
|
||||
#### **`client/page.tsx`**
|
||||
|
||||
- **Route**: `/client`
|
||||
- **Fungsi**: Dashboard utama klien
|
||||
- **Fitur**:
|
||||
- Menampilkan program latihan yang ditugaskan
|
||||
- Tombol navigasi ke mode Training, Free Mode, dan Monitor
|
||||
- Informasi pelatih yang ditugaskan
|
||||
|
||||
#### **`client/free/`**
|
||||
|
||||
##### **`client/free/page.tsx`**
|
||||
|
||||
- **Route**: `/client/free`
|
||||
- **Fungsi**: Mode latihan bebas (Personal Menu)
|
||||
- **Fitur**:
|
||||
- Builder untuk membuat menu latihan custom
|
||||
- Sistem berbasis "Round" (Set grouping)
|
||||
- Pilihan exercise terbatas ke 7 latihan inti
|
||||
- Penyimpanan ke localStorage
|
||||
- Duplicate round untuk efisiensi
|
||||
|
||||
#### **`client/monitor/`**
|
||||
|
||||
##### **`client/monitor/page.tsx`**
|
||||
|
||||
- **Route**: `/client/monitor`
|
||||
- **Fungsi**: Monitor real-time untuk form exercise
|
||||
- **Fitur**:
|
||||
- Live camera feed dengan pose detection
|
||||
- Visual feedback untuk form quality
|
||||
- Counter repetisi real-time
|
||||
- Tidak ada target/assignment
|
||||
|
||||
#### **`client/training/`**
|
||||
|
||||
##### **`client/training/page.tsx`**
|
||||
|
||||
- **Route**: `/client/training`
|
||||
- **Fungsi**: Mode latihan utama dengan program assigned
|
||||
- **Fitur**:
|
||||
- AI-powered form tracking per rep
|
||||
- Automatic rep counting
|
||||
- Rest timer antar set
|
||||
- **Training Recap**:
|
||||
- Summary statistik (total sets, total reps)
|
||||
- Table detail per set dengan:
|
||||
- Form score average
|
||||
- Expandable per-rep breakdown
|
||||
- Grade labels (Excellent/Good/Fair/Needs Improvement)
|
||||
- Feedback text spesifik per rep (e.g., "Knees Inward")
|
||||
- Toggle antara Assigned Program dan Personal Menu
|
||||
- Reset functionality
|
||||
|
||||
---
|
||||
|
||||
### 3. **`coach/` - Halaman Pelatih**
|
||||
|
||||
Folder untuk semua halaman yang diakses oleh pengguna dengan role "Coach".
|
||||
|
||||
#### **`coach/dashboard/`**
|
||||
|
||||
##### **`coach/dashboard/page.tsx`**
|
||||
|
||||
- **Route**: `/coach/dashboard`
|
||||
- **Fungsi**: Dashboard utama pelatih
|
||||
- **Fitur**:
|
||||
- Daftar klien yang terhubung
|
||||
- Link Client functionality
|
||||
- Navigasi ke menu management
|
||||
|
||||
#### **`coach/menu/`**
|
||||
|
||||
Folder untuk manajemen menu latihan.
|
||||
|
||||
##### **`coach/menu/[id]/`**
|
||||
|
||||
- **Route**: `/coach/menu/[id]`
|
||||
- **Fungsi**: Edit menu latihan yang sudah ada
|
||||
- **Fitur**:
|
||||
- Form editor untuk menu details
|
||||
- Exercise composer dengan round system
|
||||
- Preview menu structure
|
||||
- Update dan delete operations
|
||||
|
||||
##### **`coach/menu/new/`**
|
||||
|
||||
- **File**: `page.tsx`
|
||||
- **Route**: `/coach/menu/new`
|
||||
- **Fungsi**: Membuat menu latihan baru
|
||||
- **Fitur**:
|
||||
- Round-based exercise builder
|
||||
- Input untuk:
|
||||
- Nama menu
|
||||
- Target reps per exercise
|
||||
- Weight (beban)
|
||||
- Rest time
|
||||
- Pilihan 7 exercise core
|
||||
- Duplicate round
|
||||
- Group sets by exercise
|
||||
- Auto-save to database
|
||||
|
||||
#### **`coach/recap/`**
|
||||
|
||||
##### **`coach/recap/[id]/`**
|
||||
|
||||
- **Route**: `/coach/recap/[id]`
|
||||
- **Fungsi**: Melihat detail recap latihan klien
|
||||
- **Fitur**:
|
||||
- Visualisasi statistik klien
|
||||
- Form quality analysis
|
||||
- Historical performance tracking
|
||||
|
||||
---
|
||||
|
||||
### 4. **`debug/`**
|
||||
|
||||
- **Fungsi**: Halaman untuk debugging dan testing
|
||||
- **Status**: Development/testing only
|
||||
- **Akses**: Biasanya disabled di production
|
||||
|
||||
---
|
||||
|
||||
### 5. **`generated/` - Prisma Client**
|
||||
|
||||
Folder yang di-generate otomatis oleh Prisma ORM.
|
||||
|
||||
#### **`generated/client/`**
|
||||
|
||||
- **File-file utama**:
|
||||
- `client.ts` - Prisma Client instance
|
||||
- `browser.ts` - Browser-compatible client
|
||||
- `models.ts` - Type definitions untuk database models
|
||||
- `enums.ts` - Enum definitions
|
||||
- `commonInputTypes.ts` - Input type definitions
|
||||
- `libquery_engine-*.so.node` - Native query engine binary
|
||||
|
||||
- **Folder `internal/`**: Internal Prisma helpers
|
||||
- **Folder `models/`**: Model type definitions
|
||||
|
||||
**PENTING**: ❌ Jangan edit manual, akan di-regenerate saat `prisma generate`
|
||||
|
||||
---
|
||||
|
||||
### 6. **`lib/` (Symlink)**
|
||||
|
||||
- **Fungsi**: Symlink ke `/lib` di root project
|
||||
- **Isi**: Helper functions dan utility libraries
|
||||
- **Contoh isi**:
|
||||
- `pose/HARCore.ts` - Human Activity Recognition core
|
||||
- `pose/RehabCore.ts` - Exercise recognition dan rep counting
|
||||
- Form analysis algorithms
|
||||
|
||||
---
|
||||
|
||||
### 7. **`login/`**
|
||||
|
||||
- **File**: Kemungkinan `page.tsx` (route file)
|
||||
- **Route**: `/login`
|
||||
- **Fungsi**: Halaman login untuk semua pengguna
|
||||
- **Fitur**:
|
||||
- Form email dan password
|
||||
- Authentication via NextAuth
|
||||
- Redirect ke dashboard sesuai role
|
||||
|
||||
---
|
||||
|
||||
### 8. **`register/`**
|
||||
|
||||
- **File**: `page.tsx`
|
||||
- **Route**: `/register`
|
||||
- **Fungsi**: Halaman registrasi pengguna baru
|
||||
- **Fitur**:
|
||||
- Form input: nama, email, password
|
||||
- Pilihan role (Coach/Client)
|
||||
- Validasi input
|
||||
- Submit ke `/api/register`
|
||||
|
||||
---
|
||||
|
||||
## 📄 File Konfigurasi Root
|
||||
|
||||
### **`favicon.ico`**
|
||||
|
||||
- **Fungsi**: Icon aplikasi yang muncul di browser tab
|
||||
- **Format**: ICO file
|
||||
- **Size**: 25.9 KB
|
||||
|
||||
### **`globals.css`**
|
||||
|
||||
- **Fungsi**: Global stylesheet untuk seluruh aplikasi
|
||||
- **Isi**:
|
||||
- Reset CSS
|
||||
- Tailwind CSS directives (@tailwind base, components, utilities)
|
||||
- Custom color variables
|
||||
- Global typography styles
|
||||
- **Size**: ~773 bytes
|
||||
|
||||
### **`layout.tsx`**
|
||||
|
||||
- **Fungsi**: Root layout component (App-wide wrapper)
|
||||
- **Fitur**:
|
||||
- HTML structure (`<html>`, `<body>`)
|
||||
- Metadata configuration
|
||||
- Font loading (biasanya)
|
||||
- Global providers (Session, Theme, etc.)
|
||||
- **Applies to**: Semua route di aplikasi
|
||||
- **Size**: ~757 bytes
|
||||
|
||||
### **`page.tsx`**
|
||||
|
||||
- **Route**: `/` (Homepage)
|
||||
- **Fungsi**: Landing page aplikasi
|
||||
- **Fitur**:
|
||||
- Welcome screen
|
||||
- Login/Register CTA
|
||||
- Informasi aplikasi
|
||||
- Redirect logic berdasarkan auth status
|
||||
- **Size**: ~15 KB
|
||||
|
||||
---
|
||||
|
||||
## 🔑 Poin Penting untuk Dokumentasi
|
||||
|
||||
### **Routing Convention (Next.js App Router)**
|
||||
|
||||
- `page.tsx` → Halaman yang dapat diakses
|
||||
- `route.ts` → API endpoint
|
||||
- `layout.tsx` → Layout wrapper
|
||||
- `[id]/` → Dynamic route parameter
|
||||
- `[...slug]/` → Catch-all route
|
||||
|
||||
### **Role-Based Access**
|
||||
|
||||
- `/client/*` → Hanya untuk role "Client"
|
||||
- `/coach/*` → Hanya untuk role "Coach"
|
||||
- `/api/*` → Backend endpoints (internal)
|
||||
|
||||
### **Key Features**
|
||||
|
||||
1. **AI Form Analysis**: Real-time pose detection dan rep counting
|
||||
2. **Per-Rep Tracking**: Detail skor form untuk setiap repetisi
|
||||
3. **Round System**: Exercise grouping untuk efisiensi workout builder
|
||||
4. **Training Recap**: Dashboard komprehensif pasca-latihan
|
||||
|
||||
### **Tech Stack**
|
||||
|
||||
- **Framework**: Next.js 16 (App Router)
|
||||
- **Database**: PostgreSQL via Prisma ORM
|
||||
- **Auth**: NextAuth.js
|
||||
- **UI**: TailwindCSS
|
||||
- **AI**: MediaPipe Pose Landmarker
|
||||
- **Language**: TypeScript
|
||||
|
||||
---
|
||||
|
||||
## 📊 Diagram Struktur
|
||||
|
||||
```
|
||||
app/
|
||||
├── api/ # Backend Endpoints
|
||||
│ ├── auth/
|
||||
│ ├── coach/
|
||||
│ ├── logs/
|
||||
│ ├── menus/
|
||||
│ ├── recap/
|
||||
│ ├── register/
|
||||
│ └── users/
|
||||
├── client/ # Client Dashboard & Features
|
||||
│ ├── free/ # Personal Menu Builder
|
||||
│ ├── monitor/ # Live Form Monitor
|
||||
│ ├── training/ # Assigned Training Mode
|
||||
│ └── page.tsx # Client Dashboard
|
||||
├── coach/ # Coach Dashboard & Tools
|
||||
│ ├── dashboard/
|
||||
│ ├── menu/
|
||||
│ └── recap/
|
||||
├── generated/ # Prisma Auto-Generated (DO NOT EDIT)
|
||||
├── lib/ # Utilities (Symlink)
|
||||
├── login/ # Login Page
|
||||
├── register/ # Registration Page
|
||||
├── favicon.ico
|
||||
├── globals.css # Global Styles
|
||||
├── layout.tsx # Root Layout
|
||||
└── page.tsx # Homepage
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💡 Tips Maintenance
|
||||
|
||||
1. **Jangan edit** folder `generated/` secara manual
|
||||
2. **Run** `prisma generate` setelah schema change
|
||||
3. **API routes** mengikuti REST convention
|
||||
4. **Per-rep data** disimpan dalam struktur nested di recap
|
||||
5. **Feedback text** di-capture frame-by-frame lalu dianalisa untuk dominan message
|
||||
603
DOKUMENTASI_LIB_FOLDER.md
Normal file
|
|
@ -0,0 +1,603 @@
|
|||
# Dokumentasi Struktur Folder `lib/`
|
||||
|
||||
## Gambaran Umum
|
||||
|
||||
Folder `lib/` berisi semua utility libraries, helper functions, dan core logic aplikasi. Ini adalah "otak" dari aplikasi yang menangani AI pose detection, exercise recognition, authentication, dan database operations.
|
||||
|
||||
---
|
||||
|
||||
## 📁 Struktur Direktori
|
||||
|
||||
```
|
||||
lib/
|
||||
├── auth.tsx # Authentication Context & Utilities
|
||||
├── mediapipe-shim.js # MediaPipe Polyfill
|
||||
├── prisma.ts # Prisma Client Instance
|
||||
├── pose/ # AI & Exercise Recognition Engine
|
||||
│ ├── ExerciseRules.ts # Exercise Configuration & Rules
|
||||
│ ├── HARCore.ts # Human Activity Recognition Core
|
||||
│ ├── MathUtils.ts # Mathematical Utilities
|
||||
│ ├── RehabCore.ts # Exercise Recognition & Counting
|
||||
│ ├── RehabFSM.ts # Finite State Machines for Reps
|
||||
│ ├── RepetitionCounter.ts # Legacy Rep Counter
|
||||
│ └── XGBoostPredictor.ts # ML Model Predictor
|
||||
├── prisma/ # Prisma Generated Client (Symlink)
|
||||
└── prisma-gen/ # Prisma Generated Types
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📄 File Root Level
|
||||
|
||||
### **`auth.tsx`**
|
||||
|
||||
**Fungsi**: Context Provider untuk authentication dan session management
|
||||
|
||||
**Isi Utama**:
|
||||
|
||||
- **Interface**:
|
||||
- `User`: Type definition untuk user object (id, name, role, coach_id)
|
||||
- `AuthContextType`: Type untuk auth context
|
||||
- `UserRole`: 'COACH' | 'CLIENT'
|
||||
|
||||
- **Context**:
|
||||
- `AuthContext`: React Context untuk state auth global
|
||||
- `AuthProvider`: Provider component yang wrap seluruh app
|
||||
|
||||
- **Methods**:
|
||||
- `login(userId: string)`: Login user berdasarkan ID, fetch data dari `/api/users/[id]`
|
||||
- `logout()`: Clear session, hapus localStorage, reset state
|
||||
- Auto-load session dari localStorage saat app mount
|
||||
|
||||
**Tech**: React Context API, localStorage persistence
|
||||
|
||||
**Size**: ~1.8 KB
|
||||
|
||||
---
|
||||
|
||||
### **`mediapipe-shim.js`**
|
||||
|
||||
**Fungsi**: Polyfill/Shim untuk MediaPipe compatibility
|
||||
|
||||
**Isi**:
|
||||
|
||||
- Menyediakan global objects yang dibutuhkan MediaPipe di browser
|
||||
- Mengatasi module resolution issues
|
||||
- Bridge antara MediaPipe WASM dan JavaScript environment
|
||||
|
||||
**Use Case**: Diload sebelum MediaPipe library untuk ensure compatibility
|
||||
|
||||
**Size**: ~381 bytes
|
||||
|
||||
---
|
||||
|
||||
### **`prisma.ts`**
|
||||
|
||||
**Fungsi**: Singleton instance dari Prisma Client untuk database operations
|
||||
|
||||
**Isi**:
|
||||
|
||||
```typescript
|
||||
import { PrismaClient } from "../app/generated/client/client";
|
||||
|
||||
const globalForPrisma = global as unknown as { prisma: PrismaClient };
|
||||
|
||||
export const prisma = globalForPrisma.prisma || new PrismaClient();
|
||||
|
||||
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
|
||||
```
|
||||
|
||||
**Logic**:
|
||||
|
||||
- Menggunakan singleton pattern untuk avoid multiple instances
|
||||
- Di development, menyimpan instance di `global` untuk hot-reload compatibility
|
||||
- Di production, create new instance sekali saja
|
||||
|
||||
**Import Path**: `@/lib/prisma`
|
||||
|
||||
**Size**: ~300 bytes
|
||||
|
||||
---
|
||||
|
||||
## 📁 Folder `pose/` - AI Exercise Recognition Engine
|
||||
|
||||
Ini adalah core engine yang mendeteksi dan menganalisis gerakan exercise menggunakan AI dan heuristics.
|
||||
|
||||
### **`ExerciseRules.ts`**
|
||||
|
||||
**Fungsi**: Konfigurasi dan aturan untuk setiap jenis latihan
|
||||
|
||||
**Exports**:
|
||||
|
||||
1. **`interface Landmark`**
|
||||
- Representasi titik pose dari MediaPipe
|
||||
- Fields: `x`, `y`, `z`, `visibility?`
|
||||
|
||||
2. **`interface ExerciseConfig`**
|
||||
- Schema konfigurasi untuk setiap exercise
|
||||
- Fields:
|
||||
- `name`: Nama display
|
||||
- `detection`: Kriteria untuk mendeteksi exercise type
|
||||
- `shoulder_static`: Range angle untuk static shoulder
|
||||
- `shoulder_down`: Range untuk shoulder down
|
||||
- `hip_static`: Range untuk hip position
|
||||
- `phase_type`: 'start_down' | 'start_up' (starting position)
|
||||
- `dynamic_angles`: Range angle untuk setiap fase movement
|
||||
- Contoh: `elbow_down: [140, 180]`, `elbow_up: [0, 85]`
|
||||
- `static_angles`: Ideal angle untuk joints yang harus tetap
|
||||
- `wrist_distance`: Range jarak pergelangan tangan
|
||||
- `convex_hull`: Area coverage body saat up/down
|
||||
|
||||
3. **`EXERCISE_CONFIGS`** (Constant Object)
|
||||
- Berisi konfigurasi untuk 7 exercise core:
|
||||
1. **`bicep_curl`**: Bicep Curl
|
||||
- Phase: start_down (arms extended)
|
||||
- Dynamic: Elbow flexion 140-180° (down) → 0-85° (up)
|
||||
- Shoulder harus static (0-30°)
|
||||
2. **`hammer_curl`**: Hammer Curl
|
||||
- Similar to bicep curl
|
||||
- Wrist distance lebih ketat (0-0.2m)
|
||||
3. **`shoulder_press`**: Overhead Press
|
||||
- Phase: start_down (shoulders)
|
||||
- Elbow: 20-100° (down) → 150-180° (up)
|
||||
- Hip harus tetap 165° (standing straight)
|
||||
4. **`lateral_raises`**: Lateral Raises
|
||||
- Phase: start_down (arms at sides)
|
||||
- Shoulder: 0-30° → 80-110° (T-pose)
|
||||
- Elbow harus tetap straight (140-180°)
|
||||
5. **`squat`**: Squat
|
||||
- Phase: start_up (standing)
|
||||
- Hip/Knee: 160-180° (up) → 50-100° (down)
|
||||
6. **`deadlift`**: Deadlift
|
||||
- Phase: start_down (floor)
|
||||
- Hip: 45-100° → 160-180°
|
||||
- Elbow tetap straight (170°)
|
||||
7. **`lunges`**: Lunges
|
||||
- Phase: start_up
|
||||
- Knee: 160-180° → 70-110°
|
||||
|
||||
**Use Case**: Digunakan oleh `RehabCore` untuk validasi form dan counting reps
|
||||
|
||||
**Size**: ~5.1 KB
|
||||
|
||||
---
|
||||
|
||||
### **`HARCore.ts`**
|
||||
|
||||
**Fungsi**: Human Activity Recognition - mendeteksi aktivitas umum (Standing/Sitting/Fall)
|
||||
|
||||
**Class**: `HARCore`
|
||||
|
||||
**Fields**:
|
||||
|
||||
- `predictor: XGBoostPredictor` - ML model untuk klasifikasi
|
||||
- `rehab: RehabCore` - Exercise counter instance
|
||||
- `currentExercise: string | null` - Exercise yang sedang ditrack
|
||||
|
||||
**Methods**:
|
||||
|
||||
1. **`setExercise(name: string)`**
|
||||
- Set exercise yang akan ditrack
|
||||
- Normalize nama dari UI ke config key
|
||||
- Contoh: "Bicep Curl" → "bicep_curl"
|
||||
|
||||
2. **`resetParams()`**
|
||||
- Reset counter tanpa menghapus exercise selection
|
||||
- Calls `rehab.reset()`
|
||||
|
||||
3. **`async process(landmarks, worldLandmarks)`** ⭐ Main Method
|
||||
- **Input**:
|
||||
- `landmarks`: Array 33 pose keypoints (normalized)
|
||||
- `worldLandmarks`: 3D coordinates in meters
|
||||
- **Output**:
|
||||
```typescript
|
||||
{
|
||||
status: "Standing" | "Sitting" | "Fall Detected",
|
||||
confidence: number,
|
||||
exercise: string | null,
|
||||
reps: number,
|
||||
feedback: string,
|
||||
debug: { angles, scores }
|
||||
}
|
||||
```
|
||||
- **Logic**:
|
||||
1. Extract 141 features dari landmarks
|
||||
2. Classify activity menggunakan XGBoost
|
||||
3. Jika exercise active, process dengan RehabCore
|
||||
4. Return combined result
|
||||
|
||||
4. **`extractFeatures(landmarks): number[]`** (Private)
|
||||
- Extract 141 features:
|
||||
- **132 Raw Features**: 33 landmarks × 4 (x, y, z, visibility)
|
||||
- **9 Derived Features**:
|
||||
- 6 Angles: Left/Right Elbow, Hip, Knee
|
||||
- 2 Ratios: Shoulder width, Hip width (relative to torso)
|
||||
- 1 Alignment: Torso vertical cosine similarity
|
||||
|
||||
**Tech Stack**:
|
||||
|
||||
- XGBoost model untuk activity classification
|
||||
- Heuristic rules untuk exercise recognition
|
||||
- Angle calculation dari Math Utils
|
||||
|
||||
**Size**: ~5.9 KB
|
||||
|
||||
---
|
||||
|
||||
### **`RehabCore.ts`**
|
||||
|
||||
**Fungsi**: Exercise Recognition & Rep Counting dengan form validation
|
||||
|
||||
**Class**: `RehabCore`
|
||||
|
||||
**Fields**:
|
||||
|
||||
- `counters: { [key: string]: RepFSM[] }` - FSM instances per exercise
|
||||
- `worldLandmarksCache: Vec3[]` - Cache untuk 3D coordinates
|
||||
- `DEVIATION_THRESHOLD = 15.0` - Batas toleransi deviasi form (degrees)
|
||||
|
||||
**Counter Map**:
|
||||
|
||||
```typescript
|
||||
{
|
||||
'bicep_curl': [BicepCurlCounter('left'), BicepCurlCounter('right')],
|
||||
'hammer_curl': [HammerCurlCounter('left'), HammerCurlCounter('right')],
|
||||
'shoulder_press': [OverheadPressCounter()],
|
||||
'lateral_raises': [LateralRaiseCounter()],
|
||||
'squat': [SquatCounter()],
|
||||
'deadlift': [DeadliftCounter()],
|
||||
'lunges': [LungeCounter()]
|
||||
}
|
||||
```
|
||||
|
||||
**Methods**:
|
||||
|
||||
1. **`reset()`**
|
||||
- Reset semua counters ke initial state
|
||||
- Clear cache
|
||||
|
||||
2. **`validateExerciseType(configKey, features): string | null`**
|
||||
- ⚠️ **6-Way Wrong Exercise Detection**
|
||||
- Cek apakah pose sesuai dengan exercise yang diharapkan
|
||||
- Return error message jika salah exercise
|
||||
- Contoh: Jika program bicep curl tapi user lakukan squat → "Wrong Exercise: Detected lower body"
|
||||
|
||||
3. **`calculateDeviation(configKey, features, fsmState): { mae, isDeviating, details }`**
|
||||
- ⭐ **Per-Rep Form Scoring**
|
||||
- Hitung MAE (Mean Absolute Error) dari ideal angles
|
||||
- Compare current angles dengan config ranges
|
||||
- Return:
|
||||
- `mae`: Average deviation dalam degrees
|
||||
- `isDeviating`: Boolean (> DEVIATION_THRESHOLD)
|
||||
- `details`: Array pesan koreksi spesifik
|
||||
|
||||
4. **`process(exerciseName, landmarks, worldLandmarks, frameTime)`** ⭐ Main Method
|
||||
- **Core rep counting logic**
|
||||
- **Workflow**:
|
||||
1. Normalize exercise name
|
||||
2. Lazy-load counters jika belum ada
|
||||
3. Compute features (angles, distances, dll)
|
||||
4. **Wrong Exercise Detection**
|
||||
5. Update FSM untuk bilateral sides (left/right)
|
||||
6. **Form Deviation Analysis** (per frame)
|
||||
7. Generate composite feedback
|
||||
- **Output**:
|
||||
```typescript
|
||||
{
|
||||
left: { stage, angle, reps },
|
||||
right: { stage, angle, reps },
|
||||
scores: { deviation_mae },
|
||||
feedback: "L: UP | R: DOWN | Knees Inward 🔴"
|
||||
}
|
||||
```
|
||||
|
||||
5. **`getReps(exName): number`**
|
||||
- Get total reps (max dari left/right atau sum)
|
||||
- Untuk bilateral: ambil yang lebih besar
|
||||
- Untuk unilateral: langsung return
|
||||
|
||||
**Key Features**:
|
||||
|
||||
- ✅ Real-time rep detection menggunakan FSM
|
||||
- ✅ Form quality scoring (MAE calculation)
|
||||
- ✅ Wrong exercise detection
|
||||
- ✅ Specific corrective feedback per frame
|
||||
- ✅ Bilateral tracking (left/right independent)
|
||||
|
||||
**Size**: ~16.1 KB
|
||||
|
||||
---
|
||||
|
||||
### **`RehabFSM.ts`**
|
||||
|
||||
**Fungsi**: Finite State Machines untuk setiap jenis exercise
|
||||
|
||||
**Exports**:
|
||||
|
||||
1. **Helper Functions**:
|
||||
- `vec3(landmark): Vec3` - Convert Landmark to 3D vector
|
||||
- `computeFeatures(landmarks, worldLandmarks): PoseFeatures`
|
||||
- Extract semua angles dan distances
|
||||
- Return object dengan 20+ features
|
||||
|
||||
2. **Base Class**: `RepFSM` (Abstract)
|
||||
- Fields:
|
||||
- `state: "LOW" | "HIGH"`
|
||||
- `reps: number`
|
||||
- `lastAngle: number`
|
||||
- Methods:
|
||||
- `abstract shouldTransition(features): boolean`
|
||||
- `update(features): void`
|
||||
- `reset(): void`
|
||||
|
||||
3. **Concrete FSMs** (7 Classes):
|
||||
|
||||
**`BicepCurlCounter(side: 'left' | 'right')`**
|
||||
- Track elbow flexion angle
|
||||
- LOW (140-180°) ↔ HIGH (0-85°)
|
||||
- +1 rep saat kembali ke LOW
|
||||
|
||||
**`HammerCurlCounter(side: 'left' | 'right')`**
|
||||
- Similar to Bicep, sedikit berbeda thresholds
|
||||
|
||||
**`OverheadPressCounter()`**
|
||||
- Bilateral (min of left/right elbow)
|
||||
- LOW: hands at shoulders
|
||||
- HIGH: arms extended overhead
|
||||
|
||||
**`LateralRaiseCounter()`**
|
||||
- Track shoulder abduction (max of left/right)
|
||||
- LOW: arms at sides → HIGH: T-pose
|
||||
|
||||
**`SquatCounter()`**
|
||||
- Track hip/knee flexion (min)
|
||||
- HIGH: standing → LOW: squat depth
|
||||
|
||||
**`DeadliftCounter()`**
|
||||
- Track hip extension
|
||||
- LOW: floor position → HIGH: lockout
|
||||
|
||||
**`LungeCounter()`**
|
||||
- Track knee flexion (min of both)
|
||||
- Similar to squat
|
||||
|
||||
**Logic Pattern**:
|
||||
|
||||
```typescript
|
||||
if (state === "LOW" && angle < UP_THRESHOLD) {
|
||||
state = "HIGH";
|
||||
} else if (state === "HIGH" && angle > DOWN_THRESHOLD) {
|
||||
state = "LOW";
|
||||
reps++; // Rep completed!
|
||||
}
|
||||
```
|
||||
|
||||
**Size**: Variable (likely 5-10 KB based on 7 classes)
|
||||
|
||||
---
|
||||
|
||||
### **`MathUtils.ts`**
|
||||
|
||||
**Fungsi**: Mathematical utility functions untuk pose analysis
|
||||
|
||||
**Functions**:
|
||||
|
||||
1. **`calculateAngle(a, b, c): number`**
|
||||
- Hitung angle di point B dari triangle ABC
|
||||
- Input: 3 points dengan {x, y}
|
||||
- Output: Angle dalam degrees (0-180)
|
||||
- Formula: `arctangent` menggunakan vectors
|
||||
|
||||
2. **`calculateRangeDeviation(value, range, weight = 1.0): number`**
|
||||
- Hitung deviasi dari ideal range
|
||||
- Jika value dalam range → return 0
|
||||
- Jika di luar → return distance × weight
|
||||
- Contoh: `value=90, range=[80,100]` → return 0
|
||||
- Contoh: `value=110, range=[80,100]` → return 10
|
||||
|
||||
3. **`computeMAE(deviations): number`**
|
||||
- Mean Absolute Error
|
||||
- Average dari array deviasi
|
||||
- Digunakan untuk overall form score
|
||||
|
||||
**Use Case**:
|
||||
|
||||
- Digunakan di semua modules untuk angle calculation
|
||||
- Form validation dan scoring
|
||||
|
||||
**Size**: ~500 bytes (estimasi)
|
||||
|
||||
---
|
||||
|
||||
### **`XGBoostPredictor.ts`**
|
||||
|
||||
**Fungsi**: XGBoost Machine Learning model predictor untuk activity classification
|
||||
|
||||
**Class**: `XGBoostPredictor`
|
||||
|
||||
**Purpose**:
|
||||
|
||||
- Classify pose sebagai "Standing", "Sitting", atau "Fall Detected"
|
||||
- Menggunakan pre-trained XGBoost model
|
||||
|
||||
**Methods**:
|
||||
|
||||
- `predict(features: number[]): number[]`
|
||||
- Input: 141 features dari `HARCore.extractFeatures()`
|
||||
- Output: Probability array [standingProb, sittingProb, fallProb]
|
||||
- Model di-load dari embedded weights
|
||||
|
||||
**Tech**:
|
||||
|
||||
- XGBoost model serialized ke JavaScript
|
||||
- Likely menggunakan ONNX atau custom JSON format
|
||||
|
||||
**Size**: Likely large (10-50 KB) karena contain model weights
|
||||
|
||||
---
|
||||
|
||||
### **`RepetitionCounter.ts`**
|
||||
|
||||
**Fungsi**: Legacy rep counter (kemungkinan deprecated)
|
||||
|
||||
**Status**: ⚠️ Likely tidak digunakan, digantikan oleh `RehabFSM.ts`
|
||||
|
||||
**Reason**:
|
||||
|
||||
- `RehabCore` menggunakan FSM dari `RehabFSM.ts`
|
||||
- File ini mungkin versi lama sebelum refactor
|
||||
|
||||
---
|
||||
|
||||
## 📁 Folder `prisma/` dan `prisma-gen/`
|
||||
|
||||
**Status**: Auto-generated by Prisma CLI
|
||||
|
||||
### **`prisma/client/`**
|
||||
|
||||
- Symlink atau copy dari `/app/generated/client`
|
||||
- Prisma Client untuk browser compatibility
|
||||
|
||||
### **`prisma-gen/`**
|
||||
|
||||
- Type definitions dan models
|
||||
- Files:
|
||||
- `browser.ts` - Browser-compatible client
|
||||
- `client.ts` - Main client
|
||||
- `models.ts` - Type definitions
|
||||
- `enums.ts` - Enum types
|
||||
- `models/` - Individual model files:
|
||||
- `activity_logs.ts`
|
||||
- `training_menus.ts`
|
||||
- `user_recaps.ts`
|
||||
|
||||
**PENTING**: ❌ **JANGAN EDIT MANUAL** - Will be regenerated by `prisma generate`
|
||||
|
||||
---
|
||||
|
||||
## 🔑 Key Concepts & Data Flow
|
||||
|
||||
### **Complete Exercise Recognition Flow**:
|
||||
|
||||
```
|
||||
1. MediaPipe Pose Detection
|
||||
└─> 33 Landmarks (x, y, z, visibility)
|
||||
|
||||
2. HARCore.process()
|
||||
├─> Extract 141 Features
|
||||
├─> XGBoost Prediction (Standing/Sitting/Fall)
|
||||
└─> RehabCore.process()
|
||||
├─> Compute Angles & Features
|
||||
├─> Wrong Exercise Detection
|
||||
├─> FSM Update (Rep Counting)
|
||||
├─> Form Deviation Calculation (MAE)
|
||||
└─> Generate Feedback Text
|
||||
|
||||
3. Return to UI
|
||||
└─> { status, reps, feedback, scores: {deviation_mae} }
|
||||
```
|
||||
|
||||
### **Per-Rep Tracking Flow** (NEW):
|
||||
|
||||
```
|
||||
Training Page (client/training/page.tsx)
|
||||
├─> Frame Loop: predictWebcam()
|
||||
│ ├─> Call har.process()
|
||||
│ ├─> Accumulate MAE to repBuffer
|
||||
│ └─> Capture feedback to repFeedbackBuffer
|
||||
│
|
||||
├─> Rep Completion Detection (res.reps > lastRepCount)
|
||||
│ ├─> Calculate avgRepScore
|
||||
│ ├─> Find dominant feedback
|
||||
│ └─> Push to currentSetReps[{rep, score, feedback}]
|
||||
│
|
||||
└─> Set Completion
|
||||
├─> Save to results state
|
||||
└─> API POST to /api/recap
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💡 Important Notes
|
||||
|
||||
### **Exercise Configuration**:
|
||||
|
||||
- Semua config di `ExerciseRules.ts`
|
||||
- Untuk tambah exercise baru:
|
||||
1. Add config di `EXERCISE_CONFIGS`
|
||||
2. Create FSM class di `RehabFSM.ts`
|
||||
3. Add entry di `COUNTER_MAP` di `RehabCore.ts`
|
||||
|
||||
### **Form Scoring**:
|
||||
|
||||
- MAE (Mean Absolute Error) dalam degrees
|
||||
- Thresholds:
|
||||
- < 8° = **Excellent**
|
||||
- < 15° = **Good**
|
||||
- < 25° = **Fair**
|
||||
- ≥ 25° = **Needs Improvement**
|
||||
|
||||
### **Wrong Exercise Detection**:
|
||||
|
||||
- 6-way classification:
|
||||
1. Upper body dynamic (curls/press)
|
||||
2. Upper body static T-pose (lateral raises)
|
||||
3. Lower body squat pattern
|
||||
4. Lower body hinge pattern (deadlift)
|
||||
5. Lower body lunge pattern
|
||||
6. Unrecognized pose
|
||||
|
||||
### **Performance**:
|
||||
|
||||
- Frame processing: ~10-30ms
|
||||
- Feature extraction: O(n) dengan n=33 landmarks
|
||||
- FSM updates: O(1) per exercise
|
||||
- Real-time capable at 30 FPS
|
||||
|
||||
---
|
||||
|
||||
## 📊 Dependencies
|
||||
|
||||
```
|
||||
lib/pose/
|
||||
├── ExerciseRules.ts (Pure config, no deps)
|
||||
├── MathUtils.ts (Pure math, no deps)
|
||||
├── RehabFSM.ts
|
||||
│ └── depends on: ExerciseRules, MathUtils
|
||||
├── RehabCore.ts
|
||||
│ └── depends on: ExerciseRules, RehabFSM, MathUtils
|
||||
├── XGBoostPredictor.ts (Standalone ML model)
|
||||
└── HARCore.ts
|
||||
└── depends on: ALL above
|
||||
```
|
||||
|
||||
```
|
||||
lib/ (Root)
|
||||
├── auth.tsx (React Context)
|
||||
├── prisma.ts (Database client)
|
||||
└── mediapipe-shim.js (Standalone polyfill)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Maintenance Tips
|
||||
|
||||
1. **Update Exercise Config**: Edit `ExerciseRules.ts` → Test di Monitor mode
|
||||
2. **Adjust Thresholds**: Tweak angle ranges di config atau FSM classes
|
||||
3. **Debug Form Scores**: Check `res.debug.scores` di browser console
|
||||
4. **Add New Exercise**:
|
||||
- Step 1: Config di `ExerciseRules.ts`
|
||||
- Step 2: FSM class di `RehabFSM.ts`
|
||||
- Step 3: Register di `RehabCore.ts` COUNTER_MAP
|
||||
- Step 4: Update UI exercise dropdown
|
||||
5. **Performance Tuning**: Optimize `extractFeatures()` di HARCore jika lag
|
||||
|
||||
---
|
||||
|
||||
## 📈 Future Enhancements
|
||||
|
||||
- [ ] Add more exercises (Pull-ups, Push-ups, etc.)
|
||||
- [ ] Implement velocity-based training (track rep speed)
|
||||
- [ ] Add form correction animations/overlays
|
||||
- [ ] Machine learning untuk auto-tune thresholds per user
|
||||
- [ ] Export workout data ke CSV/PDF
|
||||
795
DOKUMENTASI_PRISMA_FOLDER.md
Normal file
|
|
@ -0,0 +1,795 @@
|
|||
# Dokumentasi Folder `prisma/`
|
||||
|
||||
## Gambaran Umum
|
||||
|
||||
Folder `prisma/` berisi semua konfigurasi dan migration files untuk database management menggunakan Prisma ORM. Folder ini adalah "blueprint" dari database schema aplikasi.
|
||||
|
||||
---
|
||||
|
||||
## 📁 Struktur Direktori
|
||||
|
||||
```
|
||||
prisma/
|
||||
├── migrations/ # Database Migration History
|
||||
│ ├── 20251228085634_add_users_model/
|
||||
│ │ └── migration.sql # Initial database schema
|
||||
│ ├── 20251228091450_add_menu_client_assignment/
|
||||
│ │ └── migration.sql # Menu assignment feature
|
||||
│ └── migration_lock.toml # Migration lock file
|
||||
├── schema.prisma # Database Schema Definition
|
||||
└── seed.ts # Database Seeder Script
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📄 File Utama
|
||||
|
||||
### **`schema.prisma`** ⭐
|
||||
|
||||
**Fungsi**: Schema definition file - Blueprint utama database
|
||||
|
||||
**Location**: `/prisma/schema.prisma`
|
||||
|
||||
**Size**: ~2.6 KB
|
||||
|
||||
#### **Generator Configuration**:
|
||||
|
||||
```prisma
|
||||
generator client {
|
||||
provider = "prisma-client"
|
||||
output = "../app/generated/client"
|
||||
}
|
||||
```
|
||||
|
||||
- Generate Prisma Client TypeScript
|
||||
- Output ke `app/generated/client/` untuk import di aplikasi
|
||||
|
||||
#### **Datasource Configuration**:
|
||||
|
||||
```prisma
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
```
|
||||
|
||||
- Database: PostgreSQL
|
||||
- Connection string dari environment variable `DATABASE_URL`
|
||||
|
||||
---
|
||||
|
||||
## 📊 Database Models
|
||||
|
||||
### **1. Model `users`** 👥
|
||||
|
||||
**Fungsi**: Menyimpan data pengguna (Coach & Client)
|
||||
|
||||
**Fields**:
|
||||
| Field | Type | Description | Constraint |
|
||||
|-------|------|-------------|------------|
|
||||
| `id` | String | User ID unik | Primary Key, default: cuid() |
|
||||
| `name` | String | Nama lengkap | VARCHAR, NOT NULL |
|
||||
| `role` | String | Role pengguna | VARCHAR, "COACH" atau "CLIENT" |
|
||||
| `coach_id` | String? | ID coach (untuk client) | Foreign Key → users.id |
|
||||
| `created_at` | DateTime? | Waktu registrasi | Default: now() |
|
||||
|
||||
**Relations**:
|
||||
|
||||
- `coach` → Self-relation ke `users` (Many-to-One)
|
||||
- Client memiliki 1 coach
|
||||
- `clients` → Self-relation ke `users` (One-to-Many)
|
||||
- Coach memiliki banyak clients
|
||||
- `created_menus` → `training_menus[]` (One-to-Many)
|
||||
- Coach dapat membuat banyak menu
|
||||
- `assigned_menus` → `training_menus[]` (One-to-Many)
|
||||
- Client dapat memiliki banyak assigned menus
|
||||
- `recaps` → `user_recaps[]` (One-to-Many)
|
||||
- User dapat memiliki banyak workout recaps
|
||||
- `activity_logs` → `activity_logs[]` (One-to-Many)
|
||||
- User dapat memiliki banyak activity logs
|
||||
|
||||
**Indexes**:
|
||||
|
||||
- `ix_users_id` on `id` (Primary)
|
||||
- `ix_users_coach_id` on `coach_id` (Query optimization)
|
||||
|
||||
**ID Format**:
|
||||
|
||||
- Coach: `C00001`, `C00002`, etc.
|
||||
- Client: `U00001`, `U00002`, etc.
|
||||
|
||||
**Business Logic**:
|
||||
|
||||
```typescript
|
||||
// Coach can have multiple clients
|
||||
Coach (C00001)
|
||||
└─> Clients: [U00001, U00002]
|
||||
|
||||
// Client belongs to one coach
|
||||
Client (U00001)
|
||||
└─> Coach: C00001
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **2. Model `activity_logs`** 📝
|
||||
|
||||
**Fungsi**: Log aktivitas real-time (Standing/Sitting/Fall detection)
|
||||
|
||||
**Fields**:
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `id` | Int | Auto-increment ID (Primary Key) |
|
||||
| `timestamp` | DateTime? | Waktu log dibuat |
|
||||
| `status` | String? | "Standing", "Sitting", "Fall Detected" |
|
||||
| `confidence` | String? | Confidence score dari XGBoost |
|
||||
| `details` | Json? | Additional metadata |
|
||||
| `user_id` | String? | Foreign Key → users.id |
|
||||
|
||||
**Relations**:
|
||||
|
||||
- `user` → `users` (Many-to-One)
|
||||
|
||||
**Indexes**:
|
||||
|
||||
- `ix_activity_logs_id` on `id`
|
||||
- `ix_activity_logs_user_id` on `user_id`
|
||||
|
||||
**Use Case**:
|
||||
|
||||
- Real-time monitoring di `/client/monitor`
|
||||
- Historical activity tracking
|
||||
- Fall detection alerts
|
||||
|
||||
**Example Data**:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"timestamp": "2025-12-28T10:30:00Z",
|
||||
"status": "Standing",
|
||||
"confidence": "0.95",
|
||||
"details": {
|
||||
"exercise": "bicep_curl",
|
||||
"reps": 5
|
||||
},
|
||||
"user_id": "U00001"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **3. Model `training_menus`** 📋
|
||||
|
||||
**Fungsi**: Menyimpan workout programs/menus yang dibuat coach
|
||||
|
||||
**Fields**:
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `id` | Int | Auto-increment ID (Primary Key) |
|
||||
| `name` | String? | Nama menu (e.g., "Upper Body Day 1") |
|
||||
| `exercises` | Json? | Array of exercise objects |
|
||||
| `created_at` | DateTime? | Waktu pembuatan |
|
||||
| `author_id` | String? | Foreign Key → users.id (Coach yang buat) |
|
||||
| `client_id` | String? | Foreign Key → users.id (Client assigned) |
|
||||
|
||||
**Relations**:
|
||||
|
||||
- `author` → `users` (Many-to-One, relation: "CreatedMenus")
|
||||
- Menu dibuat oleh 1 coach
|
||||
- `assigned_client` → `users` (Many-to-One, relation: "AssignedMenus")
|
||||
- Menu ditugaskan kepada 1 client
|
||||
- `user_recaps` → `user_recaps[]` (One-to-Many)
|
||||
- Menu bisa memiliki banyak recap results
|
||||
|
||||
**Indexes**:
|
||||
|
||||
- `ix_training_menus_id` on `id`
|
||||
- `ix_training_menus_name` on `name`
|
||||
- `ix_training_menus_author_id` on `author_id`
|
||||
|
||||
**JSON Structure untuk `exercises` field**:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"name": "Bicep Curl",
|
||||
"set_index": 1,
|
||||
"reps": 10,
|
||||
"weight": 15,
|
||||
"rest": 60
|
||||
},
|
||||
{
|
||||
"name": "Hammer Curl",
|
||||
"set_index": 1,
|
||||
"reps": 12,
|
||||
"weight": 12,
|
||||
"rest": 60
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
**Business Logic**:
|
||||
|
||||
```typescript
|
||||
// Coach creates menu
|
||||
Coach (C00001)
|
||||
└─> Creates Menu (id: 1, "Upper Body")
|
||||
└─> Assigns to Client (U00001)
|
||||
|
||||
// Client sees assigned menu
|
||||
Client (U00001)
|
||||
└─> Assigned Menu: "Upper Body" (id: 1)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **4. Model `user_recaps`** 📈
|
||||
|
||||
**Fungsi**: Menyimpan hasil latihan (training recap) setelah workout selesai
|
||||
|
||||
**Fields**:
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `id` | Int | Auto-increment ID (Primary Key) |
|
||||
| `menu_id` | Int? | Foreign Key → training_menus.id |
|
||||
| `user_id` | String? | Foreign Key → users.id |
|
||||
| `summary` | Json? | Workout summary data |
|
||||
| `completed_at` | DateTime? | Waktu workout selesai |
|
||||
|
||||
**Relations**:
|
||||
|
||||
- `training_menus` → `training_menus` (Many-to-One)
|
||||
- Recap terkait dengan 1 menu
|
||||
- `user` → `users` (Many-to-One)
|
||||
- Recap milik 1 user
|
||||
|
||||
**Indexes**:
|
||||
|
||||
- `ix_user_recaps_id` on `id`
|
||||
- `ix_user_recaps_user_id` on `user_id`
|
||||
|
||||
**JSON Structure untuk `summary` field**:
|
||||
|
||||
```json
|
||||
{
|
||||
"completed": true,
|
||||
"exercises": [
|
||||
{
|
||||
"name": "Bicep Curl",
|
||||
"set_index": 1,
|
||||
"reps": 10,
|
||||
"weight": 15,
|
||||
"rest": 60
|
||||
}
|
||||
],
|
||||
"timestamp": "2025-12-28T11:00:00Z",
|
||||
"results": [
|
||||
{
|
||||
"name": "Bicep Curl",
|
||||
"set": 1,
|
||||
"reps": 10,
|
||||
"weight": 15,
|
||||
"score": 12.5,
|
||||
"repDetails": [
|
||||
{
|
||||
"rep": 1,
|
||||
"score": 8.2,
|
||||
"feedback": "Perfect"
|
||||
},
|
||||
{
|
||||
"rep": 2,
|
||||
"score": 14.5,
|
||||
"feedback": "Elbow moving forward"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Key Features di Summary**:
|
||||
|
||||
- ✅ Overall workout stats
|
||||
- ✅ Per-set average form scores
|
||||
- ✅ **Per-rep breakdown** dengan:
|
||||
- Rep number
|
||||
- MAE score
|
||||
- Specific feedback text
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Database Relationships Diagram
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
users ||--o{ users : "coach-client"
|
||||
users ||--o{ training_menus : "creates (author)"
|
||||
users ||--o{ training_menus : "assigned to (client)"
|
||||
users ||--o{ user_recaps : "performs workout"
|
||||
users ||--o{ activity_logs : "generates logs"
|
||||
training_menus ||--o{ user_recaps : "tracked in recap"
|
||||
|
||||
users {
|
||||
string id PK
|
||||
string name
|
||||
string role
|
||||
string coach_id FK
|
||||
}
|
||||
|
||||
training_menus {
|
||||
int id PK
|
||||
string name
|
||||
json exercises
|
||||
string author_id FK
|
||||
string client_id FK
|
||||
}
|
||||
|
||||
user_recaps {
|
||||
int id PK
|
||||
int menu_id FK
|
||||
string user_id FK
|
||||
json summary
|
||||
}
|
||||
|
||||
activity_logs {
|
||||
int id PK
|
||||
string status
|
||||
string user_id FK
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📁 Folder `migrations/`
|
||||
|
||||
### **Gambaran Umum**
|
||||
|
||||
Folder yang berisi history semua perubahan database schema.
|
||||
|
||||
### **Migration Files**:
|
||||
|
||||
#### **1. `20251228085634_add_users_model/migration.sql`**
|
||||
|
||||
**Tanggal**: 28 Desember 2025, 08:56:34
|
||||
|
||||
**Fungsi**: Initial database schema creation
|
||||
|
||||
**Changes**:
|
||||
|
||||
- ✅ Create table `users` dengan self-referencing foreign key
|
||||
- ✅ Create table `activity_logs` dengan JSONB details
|
||||
- ✅ Create table `training_menus` dengan JSONB exercises
|
||||
- ✅ Create table `user_recaps` dengan JSONB summary
|
||||
- ✅ Create indexes untuk optimization:
|
||||
- User ID index
|
||||
- Coach ID index
|
||||
- Menu name index
|
||||
- Author ID index
|
||||
- User recap user_id index
|
||||
- ✅ Add foreign key constraints dengan proper ON DELETE/UPDATE actions
|
||||
|
||||
**Key SQL**:
|
||||
|
||||
```sql
|
||||
CREATE TABLE "users" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" VARCHAR NOT NULL,
|
||||
"role" VARCHAR NOT NULL,
|
||||
"coach_id" TEXT,
|
||||
"created_at" TIMESTAMP(6) DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- Self-referencing foreign key
|
||||
ALTER TABLE "users"
|
||||
ADD CONSTRAINT "users_coach_id_fkey"
|
||||
FOREIGN KEY ("coach_id") REFERENCES "users"("id")
|
||||
ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
```
|
||||
|
||||
**Size**: ~2.7 KB
|
||||
|
||||
---
|
||||
|
||||
#### **2. `20251228091450_add_menu_client_assignment/migration.sql`**
|
||||
|
||||
**Tanggal**: 28 Desember 2025, 09:14:50
|
||||
|
||||
**Fungsi**: (Merged into previous migration)
|
||||
|
||||
**Status**: ⚠️ Skipped/No-op migration
|
||||
|
||||
**Reason**:
|
||||
|
||||
- Original purpose: Add `client_id` to `training_menus`
|
||||
- Already included in first migration to fix type mismatches
|
||||
- File kept untuk preserve migration history order
|
||||
|
||||
**Content**:
|
||||
|
||||
```sql
|
||||
-- This migration is skipped because its changes (adding client_id)
|
||||
-- were manually merged into the previous migration to fix type mismatches
|
||||
-- This file is kept to preserve migration history order.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### **`migration_lock.toml`**
|
||||
|
||||
**Fungsi**: Lock file untuk ensure consistent database provider
|
||||
|
||||
**Content**:
|
||||
|
||||
```toml
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (e.g., Git)
|
||||
provider = "postgresql"
|
||||
```
|
||||
|
||||
**Purpose**:
|
||||
|
||||
- Mencegah accidental switch ke database provider lain
|
||||
- Ensure semua developer/environment menggunakan PostgreSQL
|
||||
- Auto-generated oleh Prisma CLI
|
||||
|
||||
---
|
||||
|
||||
## 📄 File `seed.ts`
|
||||
|
||||
**Fungsi**: Database seeder untuk populate initial test data
|
||||
|
||||
**Location**: `/prisma/seed.ts`
|
||||
|
||||
**Size**: ~1.6 KB
|
||||
|
||||
### **Seed Data**:
|
||||
|
||||
**2 Coaches**:
|
||||
|
||||
1. `C00001` - "Coach One"
|
||||
2. `C00002` - "Coach Two"
|
||||
|
||||
**3 Clients**:
|
||||
|
||||
1. `U00001` - "Client One" (assigned to Coach One)
|
||||
2. `U00002` - "Client Two" (assigned to Coach One)
|
||||
3. `U00003` - "Client Three" (assigned to Coach Two)
|
||||
|
||||
### **Script Logic**:
|
||||
|
||||
```typescript
|
||||
import { PrismaClient } from "../app/generated/client/client";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
// Upsert coaches
|
||||
const coach1 = await prisma.users.upsert({
|
||||
where: { id: "C00001" },
|
||||
update: {},
|
||||
create: {
|
||||
id: "C00001",
|
||||
name: "Coach One",
|
||||
role: "COACH",
|
||||
},
|
||||
});
|
||||
|
||||
// Upsert clients with coach_id
|
||||
const client1 = await prisma.users.upsert({
|
||||
where: { id: "U00001" },
|
||||
update: {},
|
||||
create: {
|
||||
id: "U00001",
|
||||
name: "Client One",
|
||||
role: "CLIENT",
|
||||
coach_id: coach1.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### **Run Seeder**:
|
||||
|
||||
```bash
|
||||
npx prisma db seed
|
||||
```
|
||||
|
||||
**Use Case**:
|
||||
|
||||
- Development/testing data
|
||||
- Demo accounts
|
||||
- Quick reset database dengan data awal
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Prisma Commands
|
||||
|
||||
### **Essential Commands**:
|
||||
|
||||
#### **1. Generate Prisma Client**
|
||||
|
||||
```bash
|
||||
npx prisma generate
|
||||
```
|
||||
|
||||
- Regenerate TypeScript client dari schema
|
||||
- Run setelah setiap perubahan `schema.prisma`
|
||||
- Output ke `app/generated/client/`
|
||||
|
||||
#### **2. Create Migration**
|
||||
|
||||
```bash
|
||||
npx prisma migrate dev --name migration_name
|
||||
```
|
||||
|
||||
- Create migration file baru
|
||||
- Apply migration ke database
|
||||
- Update Prisma Client
|
||||
|
||||
#### **3. Apply Migration (Production)**
|
||||
|
||||
```bash
|
||||
npx prisma migrate deploy
|
||||
```
|
||||
|
||||
- Apply pending migrations
|
||||
- Untuk production environment
|
||||
- Tidak auto-generate client
|
||||
|
||||
#### **4. Reset Database**
|
||||
|
||||
```bash
|
||||
npx prisma migrate reset
|
||||
```
|
||||
|
||||
- Drop database
|
||||
- Re-run all migrations
|
||||
- Run seed script
|
||||
- ⚠️ **DANGER**: Deletes all data!
|
||||
|
||||
#### **5. Prisma Studio (GUI)**
|
||||
|
||||
```bash
|
||||
npx prisma studio
|
||||
```
|
||||
|
||||
- Open web GUI untuk browse/edit data
|
||||
- URL: `http://localhost:5555`
|
||||
- Visual database management
|
||||
|
||||
#### **6. Format Schema**
|
||||
|
||||
```bash
|
||||
npx prisma format
|
||||
```
|
||||
|
||||
- Auto-format `schema.prisma`
|
||||
- Fix indentation dan spacing
|
||||
|
||||
#### **7. Validate Schema**
|
||||
|
||||
```bash
|
||||
npx prisma validate
|
||||
```
|
||||
|
||||
- Check schema for errors
|
||||
- Verify relations dan syntax
|
||||
|
||||
---
|
||||
|
||||
## 🔑 Key Concepts
|
||||
|
||||
### **Migration Strategy**:
|
||||
|
||||
1. **Development**:
|
||||
- Use `prisma migrate dev`
|
||||
- Creates migration + applies + generates client
|
||||
- Safe untuk experiments
|
||||
|
||||
2. **Production**:
|
||||
- Use `prisma migrate deploy`
|
||||
- Never use `migrate dev` in prod
|
||||
- Always test migrations di staging first
|
||||
|
||||
### **Schema Best Practices**:
|
||||
|
||||
1. **Naming Conventions**:
|
||||
- Tables: `snake_case` (e.g., `training_menus`)
|
||||
- Fields: `snake_case` (e.g., `coach_id`)
|
||||
- Relations: `camelCase` di Prisma model (e.g., `assignedClient`)
|
||||
|
||||
2. **Indexes**:
|
||||
- Add index untuk foreign keys
|
||||
- Add index untuk frequently queried fields
|
||||
- Consider composite indexes untuk complex queries
|
||||
|
||||
3. **JSONB Usage**:
|
||||
- ✅ Good for: Flexible nested data (exercises, recap summary)
|
||||
- ❌ Avoid for: Searchable/filterable data
|
||||
- Use `Json` type di Prisma, becomes `JSONB` di PostgreSQL
|
||||
|
||||
4. **ID Strategy**:
|
||||
- Users: Custom strings (`C00001`, `U00001`)
|
||||
- Other tables: Auto-increment integers
|
||||
- Consider UUID untuk distributed systems
|
||||
|
||||
---
|
||||
|
||||
## 📊 Database Size Estimates
|
||||
|
||||
**Assuming Active Usage**:
|
||||
|
||||
| Table | Rows/Month | Storage |
|
||||
| ---------------- | ---------- | --------------------------- |
|
||||
| `users` | ~10 | < 1 KB |
|
||||
| `training_menus` | ~50 | ~5 KB |
|
||||
| `user_recaps` | ~500 | ~500 KB (with per-rep data) |
|
||||
| `activity_logs` | ~10,000 | ~1 MB |
|
||||
|
||||
**Total**: ~2 MB/month dengan moderate usage
|
||||
|
||||
**Optimization Tips**:
|
||||
|
||||
- Archive old `activity_logs` after 30 days
|
||||
- Compress old recaps
|
||||
- Add pagination untuk large queries
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Security Considerations
|
||||
|
||||
### **Implemented**:
|
||||
|
||||
- ✅ Foreign key constraints prevent orphaned data
|
||||
- ✅ Indexes prevent slow queries (avoid DOS)
|
||||
- ✅ ON DELETE SET NULL untuk soft deletes
|
||||
|
||||
### **TODO / Recommendations**:
|
||||
|
||||
- [ ] Add `email` field dengan `@unique` constraint
|
||||
- [ ] Add `password_hash` field (currently not in schema!)
|
||||
- [ ] Add `role` as enum type instead of string
|
||||
- [ ] Add `soft_delete` timestamp instead of hard delete
|
||||
- [ ] Add row-level security (RLS) di PostgreSQL
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Future Enhancements
|
||||
|
||||
### **Planned Schema Changes**:
|
||||
|
||||
1. **Add Authentication Fields**:
|
||||
|
||||
```prisma
|
||||
model users {
|
||||
email String @unique
|
||||
password_hash String
|
||||
email_verified Boolean @default(false)
|
||||
}
|
||||
```
|
||||
|
||||
2. **Add Workout Sessions** (untuk track progress over time):
|
||||
|
||||
```prisma
|
||||
model workout_sessions {
|
||||
id Int @id @default(autoincrement())
|
||||
user_id String
|
||||
menu_id Int
|
||||
started_at DateTime
|
||||
ended_at DateTime?
|
||||
status String // "IN_PROGRESS" | "COMPLETED" | "ABANDONED"
|
||||
}
|
||||
```
|
||||
|
||||
3. **Add Exercise Library** (normalize exercises):
|
||||
|
||||
```prisma
|
||||
model exercises {
|
||||
id Int @id @default(autoincrement())
|
||||
name String @unique
|
||||
category String // "UPPER" | "LOWER" | "CORE"
|
||||
instructions Json?
|
||||
}
|
||||
```
|
||||
|
||||
4. **Add Personal Records**:
|
||||
|
||||
```prisma
|
||||
model personal_records {
|
||||
id Int @id @default(autoincrement())
|
||||
user_id String
|
||||
exercise String
|
||||
weight_kg Float
|
||||
reps Int
|
||||
achieved_at DateTime
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💡 Troubleshooting
|
||||
|
||||
### **Common Issues**:
|
||||
|
||||
#### **1. Migration Failed**
|
||||
|
||||
```bash
|
||||
Error: Migration failed to apply
|
||||
```
|
||||
|
||||
**Solution**:
|
||||
|
||||
```bash
|
||||
# Reset database
|
||||
npx prisma migrate reset
|
||||
|
||||
# Or fix manually
|
||||
npx prisma db push --skip-generate
|
||||
npx prisma generate
|
||||
```
|
||||
|
||||
#### **2. Client Out of Sync**
|
||||
|
||||
```bash
|
||||
Error: Prisma Client is out of sync with schema
|
||||
```
|
||||
|
||||
**Solution**:
|
||||
|
||||
```bash
|
||||
npx prisma generate
|
||||
```
|
||||
|
||||
#### **3. Connection Error**
|
||||
|
||||
```bash
|
||||
Error: Can't reach database server
|
||||
```
|
||||
|
||||
**Check**:
|
||||
|
||||
1. PostgreSQL running? (`sudo systemctl status postgresql`)
|
||||
2. `DATABASE_URL` correct di `.env`?
|
||||
3. Network/firewall issues?
|
||||
|
||||
#### **4. Seed Script Fails**
|
||||
|
||||
```bash
|
||||
Error: Unique constraint violation
|
||||
```
|
||||
|
||||
**Solution**: Data already exists
|
||||
|
||||
```bash
|
||||
# Clear database first
|
||||
npx prisma migrate reset
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 Resources
|
||||
|
||||
**Official Docs**:
|
||||
|
||||
- [Prisma Schema Reference](https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference)
|
||||
- [Prisma Migrate](https://www.prisma.io/docs/concepts/components/prisma-migrate)
|
||||
- [Prisma Client API](https://www.prisma.io/docs/reference/api-reference/prisma-client-reference)
|
||||
|
||||
**Tutorials**:
|
||||
|
||||
- [Prisma Quickstart](https://www.prisma.io/docs/getting-started/quickstart)
|
||||
- [PostgreSQL Best Practices](https://wiki.postgresql.org/wiki/Best_practices)
|
||||
|
||||
---
|
||||
|
||||
## ✅ Checklist Maintenance
|
||||
|
||||
- [x] Schema documented
|
||||
- [x] Migrations tracked in Git
|
||||
- [x] Seed data available
|
||||
- [ ] Backup strategy defined
|
||||
- [ ] Migration rollback tested
|
||||
- [ ] Production deployment guide
|
||||
- [ ] Performance benchmarks
|
||||
568
DOKUMENTASI_PUBLIC_FOLDER.md
Normal file
|
|
@ -0,0 +1,568 @@
|
|||
# Dokumentasi Folder `public/`
|
||||
|
||||
## Gambaran Umum
|
||||
|
||||
Folder `public/` berisi semua static assets yang dapat diakses langsung dari browser tanpa processing. File-file di folder ini di-serve oleh Next.js dari root path `/`.
|
||||
|
||||
---
|
||||
|
||||
## 📁 Struktur Direktori
|
||||
|
||||
```
|
||||
public/
|
||||
├── models/ # Machine Learning Models
|
||||
│ └── xgb_activity_model.json # XGBoost activity classifier (2.1 MB)
|
||||
├── favicon.svg # App icon (browser tab)
|
||||
├── file.svg # File icon asset
|
||||
├── globe.svg # Globe icon asset
|
||||
├── next.svg # Next.js logo
|
||||
├── vercel.svg # Vercel logo
|
||||
└── window.svg # Window icon asset
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📄 File-File Utama
|
||||
|
||||
### **1. `models/xgb_activity_model.json`** 🤖
|
||||
|
||||
**Path**: `/public/models/xgb_activity_model.json`
|
||||
|
||||
**Fungsi**: XGBoost Machine Learning Model untuk Human Activity Recognition
|
||||
|
||||
**Purpose**:
|
||||
|
||||
- Classify pose menjadi 3 kategori:
|
||||
1. **Standing** (Berdiri)
|
||||
2. **Sitting** (Duduk)
|
||||
3. **Fall Detected** (Jatuh terdeteksi)
|
||||
|
||||
**Size**: **2.1 MB** (Very Large!)
|
||||
|
||||
**Format**: JSON serialization dari XGBoost model
|
||||
|
||||
- Berisi tree structures, weights, dan thresholds
|
||||
- trained model dari Python exported ke JavaScript-compatible format
|
||||
|
||||
**How It's Used**:
|
||||
|
||||
```typescript
|
||||
// lib/pose/XGBoostPredictor.ts
|
||||
import modelData from "../public/models/xgb_activity_model.json";
|
||||
|
||||
class XGBoostPredictor {
|
||||
predict(features: number[]): number[] {
|
||||
// Load model from JSON
|
||||
// Execute XGBoost inference
|
||||
// Return [standingProb, sittingProb, fallProb]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Access URL**: `http://localhost:3000/models/xgb_activity_model.json`
|
||||
|
||||
**Tech Details**:
|
||||
|
||||
- Input: 141 features (33 landmarks × 4 + 9 derived)
|
||||
- Output: 3-class probability distribution
|
||||
- Algorithm: Gradient Boosted Decision Trees
|
||||
|
||||
**Performance**:
|
||||
|
||||
- Inference: ~5-10ms per frame
|
||||
- Memory: ~2 MB loaded in browser
|
||||
- Accuracy: Depends on training data quality
|
||||
|
||||
---
|
||||
|
||||
### **2. `favicon.svg`** 🎨
|
||||
|
||||
**Path**: `/public/favicon.svg`
|
||||
|
||||
**Fungsi**: App icon yang muncul di browser tab
|
||||
|
||||
**Size**: ~1.1 KB
|
||||
|
||||
**Design**:
|
||||
|
||||
- SVG icon dengan gambar dumbbells (barbel)
|
||||
- Color: Blue (`#1E40AF` - blue-700)
|
||||
- Viewbox: 24×24
|
||||
- Rendered size: 48×48 pixels
|
||||
|
||||
**SVG Structure**:
|
||||
|
||||
```svg
|
||||
<svg viewBox="0 0 24 24" width="48" height="48">
|
||||
<!-- Left dumbbell weight -->
|
||||
<path d="M7.4 7H4.6..." stroke="#1E40AF"/>
|
||||
|
||||
<!-- Right dumbbell weight -->
|
||||
<path d="M19.4 7H16.6..." stroke="#1E40AF"/>
|
||||
|
||||
<!-- Center bar -->
|
||||
<path d="M8 12H16" stroke="#1E40AF"/>
|
||||
</svg>
|
||||
```
|
||||
|
||||
**Visual**: Representing fitness/strength training
|
||||
|
||||
**Browser Display**:
|
||||
|
||||
- Favicon di tab browser
|
||||
- Bookmark icon
|
||||
- Desktop shortcut icon (PWA)
|
||||
|
||||
**Access**: Automatically loaded oleh Next.js dari `<link rel="icon">`
|
||||
|
||||
---
|
||||
|
||||
### **3. Icon Assets** (SVG Icons)
|
||||
|
||||
#### **`file.svg`**
|
||||
|
||||
**Size**: ~391 bytes
|
||||
|
||||
**Fungsi**: Generic file icon
|
||||
|
||||
- Bisa digunakan untuk file upload UI
|
||||
- Document representation
|
||||
- Attachment icons
|
||||
|
||||
**Use Case**: UI components yang butuh file icon
|
||||
|
||||
---
|
||||
|
||||
#### **`globe.svg`**
|
||||
|
||||
**Size**: ~1 KB
|
||||
|
||||
**Fungsi**: Globe/world icon
|
||||
|
||||
- Public/internet representation
|
||||
- Language selection
|
||||
- Global settings
|
||||
|
||||
**Possible Uses**:
|
||||
|
||||
- Language switcher (future feature)
|
||||
- Public profile indicators
|
||||
- Network status
|
||||
|
||||
---
|
||||
|
||||
#### **`window.svg`**
|
||||
|
||||
**Size**: ~385 bytes
|
||||
|
||||
**Fungsi**: Window/application icon
|
||||
|
||||
- UI element representation
|
||||
- Modal/dialog indicators
|
||||
- Application layout icons
|
||||
|
||||
**Use Case**:
|
||||
|
||||
- Dashboard widgets
|
||||
- Window management UI
|
||||
- Layout switching
|
||||
|
||||
---
|
||||
|
||||
### **4. Brand Logos**
|
||||
|
||||
#### **`next.svg`**
|
||||
|
||||
**Size**: ~1.4 KB
|
||||
|
||||
**Fungsi**: Next.js official logo
|
||||
|
||||
- Used in default Next.js templates
|
||||
- Brand attribution
|
||||
- Developer credits
|
||||
|
||||
**Current Usage**: Likely not displayed in production
|
||||
|
||||
- Default Next.js boilerplate file
|
||||
- Can be removed if not used
|
||||
|
||||
---
|
||||
|
||||
#### **`vercel.svg`**
|
||||
|
||||
**Size**: ~128 bytes
|
||||
|
||||
**Fungsi**: Vercel deployment platform logo
|
||||
|
||||
- Hosting provider logo
|
||||
- Deployment attribution
|
||||
|
||||
**Current Usage**: Likely not displayed
|
||||
|
||||
- Boilerplate file
|
||||
- Can be removed if deploying elsewhere
|
||||
|
||||
---
|
||||
|
||||
## 🔗 How Public Files Are Accessed
|
||||
|
||||
### **In Code**:
|
||||
|
||||
```typescript
|
||||
// Direct path from root
|
||||
<img src="/favicon.svg" alt="Logo" />
|
||||
|
||||
// Model import (if using import)
|
||||
import model from '@/public/models/xgb_activity_model.json';
|
||||
|
||||
// Or fetch at runtime
|
||||
const response = await fetch('/models/xgb_activity_model.json');
|
||||
const model = await response.json();
|
||||
```
|
||||
|
||||
### **In Browser**:
|
||||
|
||||
- `http://localhost:3000/favicon.svg`
|
||||
- `http://localhost:3000/models/xgb_activity_model.json`
|
||||
- `http://localhost:3000/next.svg`
|
||||
|
||||
**Important**:
|
||||
|
||||
- ✅ NO `/public` prefix in URL
|
||||
- ✅ Files served from root `/`
|
||||
- ❌ Don't use `/public/favicon.svg` (404!)
|
||||
|
||||
---
|
||||
|
||||
## 📊 File Analysis
|
||||
|
||||
| File | Size | Type | Purpose | Status |
|
||||
| ------------------------- | ------ | -------- | ----------------------- | ---------------- |
|
||||
| `xgb_activity_model.json` | 2.1 MB | ML Model | Activity classification | ✅ **Critical** |
|
||||
| `favicon.svg` | 1.1 KB | Icon | App branding | ✅ **Active** |
|
||||
| `file.svg` | 391 B | Icon | UI asset | ⚠️ Likely unused |
|
||||
| `globe.svg` | 1 KB | Icon | UI asset | ⚠️ Likely unused |
|
||||
| `window.svg` | 385 B | Icon | UI asset | ⚠️ Likely unused |
|
||||
| `next.svg` | 1.4 KB | Logo | Boilerplate | ⚠️ Can remove |
|
||||
| `vercel.svg` | 128 B | Logo | Boilerplate | ⚠️ Can remove |
|
||||
|
||||
**Total Size**: ~2.1 MB
|
||||
|
||||
**Largest File**: `xgb_activity_model.json` (99% of total)
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Optimization Recommendations
|
||||
|
||||
### **1. Model Optimization** ⚡
|
||||
|
||||
**Current**: 2.1 MB JSON file
|
||||
|
||||
**Options**:
|
||||
|
||||
- ✅ **Lazy Load**: Only load saat dibutuhkan
|
||||
- ✅ **Compression**: Gzip/Brotli (automatic by Next.js)
|
||||
- ✅ **CDN**: Host di CDN untuk faster global access
|
||||
- ⚠️ **Quantization**: Reduce precision (may affect accuracy)
|
||||
- ⚠️ **ONNX Format**: Convert ke binary format (smaller)
|
||||
|
||||
**Lazy Load Example**:
|
||||
|
||||
```typescript
|
||||
// Load only when needed
|
||||
const loadModel = async () => {
|
||||
const model = await import("@/public/models/xgb_activity_model.json");
|
||||
return model;
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **2. Icon Cleanup** 🧹
|
||||
|
||||
**Unused Icons**: Remove jika tidak digunakan
|
||||
|
||||
```bash
|
||||
# Check usage across codebase
|
||||
grep -r "file.svg" app/
|
||||
grep -r "globe.svg" app/
|
||||
grep -r "window.svg" app/
|
||||
|
||||
# If no results, safe to delete
|
||||
rm public/file.svg public/globe.svg public/window.svg
|
||||
rm public/next.svg public/vercel.svg
|
||||
```
|
||||
|
||||
**Benefit**: Reduce deployment size, faster builds
|
||||
|
||||
---
|
||||
|
||||
### **3. Add More Assets** 📁
|
||||
|
||||
**Recommended Additions**:
|
||||
|
||||
#### **`robots.txt`**
|
||||
|
||||
```txt
|
||||
# public/robots.txt
|
||||
User-agent: *
|
||||
Allow: /
|
||||
Sitemap: https://yourapp.com/sitemap.xml
|
||||
```
|
||||
|
||||
#### **`manifest.json`** (PWA)
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Straps Fitness",
|
||||
"short_name": "Straps",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/favicon.svg",
|
||||
"sizes": "48x48",
|
||||
"type": "image/svg+xml"
|
||||
}
|
||||
],
|
||||
"theme_color": "#1E40AF",
|
||||
"background_color": "#000000",
|
||||
"display": "standalone"
|
||||
}
|
||||
```
|
||||
|
||||
#### **`sitemap.xml`** (SEO)
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<url>
|
||||
<loc>https://yourapp.com/</loc>
|
||||
<priority>1.0</priority>
|
||||
</url>
|
||||
</urlset>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔒 Security Considerations
|
||||
|
||||
### **Current Status**: ✅ Safe
|
||||
|
||||
**Why**:
|
||||
|
||||
- ✅ No sensitive data in public files
|
||||
- ✅ Model file is read-only data (not executable)
|
||||
- ✅ SVG files are static images
|
||||
|
||||
### **Best Practices**:
|
||||
|
||||
1. **Never Put Secrets Here** ❌
|
||||
- No API keys
|
||||
- No passwords
|
||||
- No private data
|
||||
2. **Validate SVG Files** ✅
|
||||
- Check for XSS vectors
|
||||
- Sanitize uploaded SVGs
|
||||
|
||||
3. **Model Integrity** 🔐
|
||||
- Verify model hash before use
|
||||
- Detect tampering
|
||||
```typescript
|
||||
const expectedHash = "sha256-abc123...";
|
||||
const actualHash = await hashFile("/models/xgb_activity_model.json");
|
||||
if (actualHash !== expectedHash) {
|
||||
throw new Error("Model tampered!");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Advanced Usage
|
||||
|
||||
### **1. Dynamic Model Loading**
|
||||
|
||||
```typescript
|
||||
// Lazy load untuk reduce initial bundle
|
||||
const HARCore = dynamic(() => import('@/lib/pose/HARCore'), {
|
||||
ssr: false,
|
||||
loading: () => <p>Loading AI Model...</p>
|
||||
});
|
||||
```
|
||||
|
||||
### **2. Service Worker Caching** (PWA)
|
||||
|
||||
```javascript
|
||||
// Cache model untuk offline use
|
||||
self.addEventListener("install", (event) => {
|
||||
event.waitUntil(
|
||||
caches.open("models-v1").then((cache) => {
|
||||
return cache.add("/models/xgb_activity_model.json");
|
||||
}),
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
### **3. Progressive Enhancement**
|
||||
|
||||
```typescript
|
||||
// Fallback jika model load gagal
|
||||
let classifier;
|
||||
try {
|
||||
classifier = await loadXGBoostModel();
|
||||
} catch (err) {
|
||||
console.warn("Model failed to load, using heuristics");
|
||||
classifier = new FallbackHeuristicClassifier();
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📈 Performance Metrics
|
||||
|
||||
### **Model Loading**:
|
||||
|
||||
- **Cold Start**: ~200-500ms (first load, no cache)
|
||||
- **Warm Start**: ~50-100ms (cached)
|
||||
- **Memory**: ~2-3 MB in-memory after parsing
|
||||
|
||||
### **Optimization Impact**:
|
||||
|
||||
```typescript
|
||||
// Before: Load synchronously (blocks rendering)
|
||||
import model from "@/public/models/xgb_activity_model.json";
|
||||
|
||||
// After: Load async (non-blocking)
|
||||
const model = await fetch("/models/xgb_activity_model.json").then((r) =>
|
||||
r.json(),
|
||||
);
|
||||
```
|
||||
|
||||
**Result**:
|
||||
|
||||
- ⚡ Faster initial page load
|
||||
- ⚡ Better Lighthouse scores
|
||||
- ⚡ Improved user experience
|
||||
|
||||
---
|
||||
|
||||
## 💡 Maintenance Tips
|
||||
|
||||
### **When Adding New Files**:
|
||||
|
||||
1. **Images**:
|
||||
- Use WebP format (smaller than PNG/JPG)
|
||||
- Optimize dengan tooling (ImageOptim, Squoosh)
|
||||
- Consider `next/image` untuk auto-optimization
|
||||
|
||||
2. **Icons**:
|
||||
- Prefer SVG (scalable, small)
|
||||
- Consider icon libraries (react-icons, lucide-react)
|
||||
- Avoid duplicates
|
||||
|
||||
3. **Data Files**:
|
||||
- Compress large JSON/CSV
|
||||
- Consider API route instead of static file
|
||||
- Use CDN untuk large files
|
||||
|
||||
### **Regular Cleanup**:
|
||||
|
||||
```bash
|
||||
# Find unused public files
|
||||
du -sh public/*
|
||||
|
||||
# Check git history untuk unused files
|
||||
git log --all --full-history -- public/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Troubleshooting
|
||||
|
||||
### **Issue**: Model not loading
|
||||
|
||||
```
|
||||
Error: Failed to fetch /models/xgb_activity_model.json
|
||||
```
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. Verify file exists: `ls public/models/`
|
||||
2. Check Next.js server running
|
||||
3. Clear browser cache
|
||||
4. Check CORS headers (if loading from external domain)
|
||||
|
||||
---
|
||||
|
||||
### **Issue**: Icons not showing
|
||||
|
||||
```
|
||||
404: /public/favicon.svg not found
|
||||
```
|
||||
|
||||
**Solution**: ❌ Remove `/public` prefix
|
||||
|
||||
```html
|
||||
<!-- Wrong -->
|
||||
<img src="/public/favicon.svg" />
|
||||
|
||||
<!-- Correct -->
|
||||
<img src="/favicon.svg" />
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **Issue**: Large bundle size
|
||||
|
||||
```
|
||||
Warning: Page size exceeds 300 KB
|
||||
```
|
||||
|
||||
**Solution**: Lazy load model
|
||||
|
||||
```typescript
|
||||
// Lazy load
|
||||
const model = await import("@/public/models/xgb_activity_model.json");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 Next.js Static File Best Practices
|
||||
|
||||
### **✅ DO**:
|
||||
|
||||
- Keep files small (< 1 MB ideally)
|
||||
- Use modern formats (WebP, AVIF, SVG)
|
||||
- Compress large files
|
||||
- Use meaningful filenames
|
||||
- Organize in subdirectories
|
||||
|
||||
### **❌ DON'T**:
|
||||
|
||||
- Put sensitive data
|
||||
- Store frequently changing data
|
||||
- Use as database replacement
|
||||
- Include source files (.psd, .ai)
|
||||
- Exceed 10 MB total size
|
||||
|
||||
---
|
||||
|
||||
## 🎓 Summary
|
||||
|
||||
**Current Status**: ✅ Well-organized, but can be optimized
|
||||
|
||||
**Key Files**:
|
||||
|
||||
1. **xgb_activity_model.json** - Critical ML model (2.1 MB)
|
||||
2. **favicon.svg** - App branding
|
||||
3. **Other SVGs** - Likely unused (cleanup recommended)
|
||||
|
||||
**Recommendations**:
|
||||
|
||||
1. ⚡ Lazy load ML model
|
||||
2. 🧹 Remove unused icons
|
||||
3. 📁 Add PWA manifest
|
||||
4. 🔐 Add model integrity check
|
||||
5. 📊 Monitor bundle size
|
||||
|
||||
**Total Size**: ~2.1 MB (99% from model file)
|
||||
|
||||
**Performance**: Good, but can be improved with lazy loading
|
||||
1017
DOKUMENTASI_ROOT_FILES.md
Normal file
779
DOKUMENTASI_SCRIPTS_FOLDER.md
Normal file
|
|
@ -0,0 +1,779 @@
|
|||
# Dokumentasi Folder `scripts/`
|
||||
|
||||
## Gambaran Umum
|
||||
|
||||
Folder `scripts/` berisi utility scripts untuk development, testing, dan database management. Scripts ini membantu developer untuk verify functionality, seed data, dan debug issues.
|
||||
|
||||
---
|
||||
|
||||
## 📁 Struktur Direktori
|
||||
|
||||
```
|
||||
scripts/
|
||||
├── check-links.ts # Check user-coach relationships
|
||||
├── check_logs.ts # View recent activity logs
|
||||
├── seed_log.ts # Create test activity log
|
||||
├── test_har_core.ts # Test activity recognition system
|
||||
└── test_rehab_core.ts # Test exercise recognition system
|
||||
```
|
||||
|
||||
**Total Files**: 5 TypeScript scripts
|
||||
|
||||
---
|
||||
|
||||
## 📄 Database Management Scripts
|
||||
|
||||
### **1. `check-links.ts`** 🔗
|
||||
|
||||
**Path**: `/scripts/check-links.ts`
|
||||
|
||||
**Fungsi**: Menampilkan semua users dan coach-client relationships
|
||||
|
||||
**Size**: ~466 bytes
|
||||
|
||||
**Use Case**:
|
||||
|
||||
- Verify seeding berhasil
|
||||
- Debug coach-client assignments
|
||||
- Quick database health check
|
||||
|
||||
**Code**:
|
||||
|
||||
```typescript
|
||||
import "dotenv/config";
|
||||
import { PrismaClient } from "../app/generated/client/client";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
const users = await prisma.users.findMany();
|
||||
console.log("--- All Users ---");
|
||||
users.forEach((u) =>
|
||||
console.log(`${u.name} (${u.role}): ID=${u.id}, CoachID=${u.coach_id}`),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**How to Run**:
|
||||
|
||||
```bash
|
||||
npx tsx scripts/check-links.ts
|
||||
```
|
||||
|
||||
**Expected Output**:
|
||||
|
||||
```
|
||||
--- All Users ---
|
||||
Coach One (COACH): ID=C00001, CoachID=null
|
||||
Coach Two (COACH): ID=C00002, CoachID=null
|
||||
Client One (CLIENT): ID=U00001, CoachID=C00001
|
||||
Client Two (CLIENT): ID=U00002, CoachID=C00001
|
||||
Client Three (CLIENT): ID=U00003, CoachID=C00002
|
||||
```
|
||||
|
||||
**Use Cases**:
|
||||
|
||||
- ✅ Verify after running `prisma db seed`
|
||||
- ✅ Check migration success
|
||||
- ✅ Debug role assignments
|
||||
- ✅ List all registered users
|
||||
|
||||
---
|
||||
|
||||
### **2. `check_logs.ts`** 📋
|
||||
|
||||
**Path**: `/scripts/check_logs.ts`
|
||||
|
||||
**Fungsi**: Menampilkan 10 activity logs terbaru dari database
|
||||
|
||||
**Size**: ~887 bytes
|
||||
|
||||
**Use Case**:
|
||||
|
||||
- Monitor real-time activity tracking
|
||||
- Debug activity logging system
|
||||
- Verify fall detection alerts
|
||||
- Inspect log details (JSON)
|
||||
|
||||
**Code**:
|
||||
|
||||
```typescript
|
||||
import { PrismaClient } from "../app/generated/client/client";
|
||||
import * as dotenv from "dotenv";
|
||||
dotenv.config();
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
console.log("Checking Activity Logs...");
|
||||
const logs = await prisma.activity_logs.findMany({
|
||||
take: 10,
|
||||
orderBy: { timestamp: "desc" },
|
||||
include: {
|
||||
user: { select: { name: true } },
|
||||
},
|
||||
});
|
||||
|
||||
if (logs.length === 0) {
|
||||
console.log("No logs found.");
|
||||
} else {
|
||||
console.log(`Found ${logs.length} logs:`);
|
||||
logs.forEach((log) => {
|
||||
console.log(
|
||||
`[${log.timestamp?.toISOString()}] ` +
|
||||
`User: ${log.user?.name || log.user_id} | ` +
|
||||
`Status: ${log.status} | ` +
|
||||
`Details: ${JSON.stringify(log.details)}`,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**How to Run**:
|
||||
|
||||
```bash
|
||||
npx tsx scripts/check_logs.ts
|
||||
```
|
||||
|
||||
**Expected Output**:
|
||||
|
||||
```
|
||||
Checking Activity Logs...
|
||||
Found 3 logs:
|
||||
[2025-12-28T10:30:00.000Z] User: Client One | Status: Standing | Details: {"exercise":"bicep_curl","reps":5}
|
||||
[2025-12-28T10:25:00.000Z] User: Client Two | Status: Sitting | Details: {}
|
||||
[2025-12-28T10:20:00.000Z] User: Client One | Status: Standing | Details: {}
|
||||
```
|
||||
|
||||
**Features**:
|
||||
|
||||
- ✅ Shows latest 10 logs (configurable via `take`)
|
||||
- ✅ Ordered by timestamp (newest first)
|
||||
- ✅ Includes user name via join
|
||||
- ✅ Pretty-prints JSON details
|
||||
|
||||
**When to Use**:
|
||||
|
||||
- After training session untuk verify logs created
|
||||
- Debug fall detection alerts
|
||||
- Monitor which users are active
|
||||
|
||||
---
|
||||
|
||||
### **3. `seed_log.ts`** 🌱
|
||||
|
||||
**Path**: `/scripts/seed_log.ts`
|
||||
|
||||
**Fungsi**: Membuat test activity log untuk development
|
||||
|
||||
**Size**: ~816 bytes
|
||||
|
||||
**Use Case**:
|
||||
|
||||
- Populate database dengan sample logs
|
||||
- Test log display UI
|
||||
- Verify logging system works
|
||||
|
||||
**Code**:
|
||||
|
||||
```typescript
|
||||
import { PrismaClient } from "../app/generated/client/client";
|
||||
import * as dotenv from "dotenv";
|
||||
dotenv.config();
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
console.log("Seeding Mock Log...");
|
||||
|
||||
// Get a client user
|
||||
const user = await prisma.users.findFirst({
|
||||
where: { role: "CLIENT" },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
console.error("No client user found to attach log to.");
|
||||
return;
|
||||
}
|
||||
|
||||
const log = await prisma.activity_logs.create({
|
||||
data: {
|
||||
user_id: user.id,
|
||||
timestamp: new Date(),
|
||||
status: "TEST_LOG",
|
||||
confidence: "1.0",
|
||||
details: { message: "Manual verification log" },
|
||||
},
|
||||
});
|
||||
|
||||
console.log("Created Log ID:", log.id);
|
||||
}
|
||||
```
|
||||
|
||||
**How to Run**:
|
||||
|
||||
```bash
|
||||
npx tsx scripts/seed_log.ts
|
||||
```
|
||||
|
||||
**Expected Output**:
|
||||
|
||||
```
|
||||
Seeding Mock Log...
|
||||
Created Log ID: 1
|
||||
```
|
||||
|
||||
**Use Cases**:
|
||||
|
||||
- ✅ Quick test untuk UI development
|
||||
- ✅ Verify database schema works
|
||||
- ✅ Populate logs tanpa perlu run full app
|
||||
|
||||
**Customization**:
|
||||
|
||||
```typescript
|
||||
// Modify untuk create specific log
|
||||
data: {
|
||||
status: 'Fall Detected', // Test fall alert
|
||||
confidence: '0.95',
|
||||
details: {
|
||||
coordinates: { x: 100, y: 200 },
|
||||
severity: 'HIGH'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Testing Scripts
|
||||
|
||||
### **4. `test_har_core.ts`** 🤖
|
||||
|
||||
**Path**: `/scripts/test_har_core.ts`
|
||||
|
||||
**Fungsi**: Unit test untuk `HARCore` activity recognition system
|
||||
|
||||
**Size**: ~1 KB
|
||||
|
||||
**Use Case**:
|
||||
|
||||
- Test XGBoost model integration
|
||||
- Verify exercise detection logic
|
||||
- Debug HAR system issues
|
||||
- Ensure setExercise() mapping works
|
||||
|
||||
**Code**:
|
||||
|
||||
```typescript
|
||||
import { HARCore } from "../lib/pose/HARCore";
|
||||
import { Landmark } from "../lib/pose/ExerciseRules";
|
||||
|
||||
const har = new HARCore();
|
||||
const mockLandmarks: Landmark[] = Array(33).fill({
|
||||
x: 0.5,
|
||||
y: 0.5,
|
||||
z: 0,
|
||||
visibility: 1,
|
||||
});
|
||||
|
||||
console.log("Testing HARCore...");
|
||||
|
||||
const inputs = ["Bicep Curl", "Squats", "deadlift", "Unknown Exercise"];
|
||||
|
||||
inputs.forEach((input) => {
|
||||
har.setExercise(input);
|
||||
console.log(`Set to '${input}'. Invoking process...`);
|
||||
|
||||
har
|
||||
.process(mockLandmarks)
|
||||
.then((res) => {
|
||||
console.log(
|
||||
`[PASS] '${input}' -> Result:`,
|
||||
res ? `Exercise: ${res.exercise}, Status: ${res.status}` : "NULL",
|
||||
);
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(`[FAIL] '${input}' -> Error:`, e);
|
||||
});
|
||||
});
|
||||
|
||||
setTimeout(() => console.log("Done."), 2000);
|
||||
```
|
||||
|
||||
**How to Run**:
|
||||
|
||||
```bash
|
||||
npx tsx scripts/test_har_core.ts
|
||||
```
|
||||
|
||||
**Expected Output**:
|
||||
|
||||
```
|
||||
Testing HARCore...
|
||||
Set to 'Bicep Curl'. Invoking process...
|
||||
Set to 'Squats'. Invoking process...
|
||||
Set to 'deadlift'. Invoking process...
|
||||
Set to 'Unknown Exercise'. Invoking process...
|
||||
[PASS] 'Bicep Curl' -> Result: Exercise: bicep_curl, Status: Standing
|
||||
[PASS] 'Squats' -> Result: Exercise: squat, Status: Standing
|
||||
[PASS] 'deadlift' -> Result: Exercise: deadlift, Status: Standing
|
||||
[PASS] 'Unknown Exercise' -> Result: Exercise: null, Status: Standing
|
||||
Done.
|
||||
```
|
||||
|
||||
**What It Tests**:
|
||||
|
||||
- ✅ Exercise name normalization (UI names → config keys)
|
||||
- ✅ Activity classification (Standing/Sitting/Fall)
|
||||
- ✅ Integration HARCore + RehabCore
|
||||
- ✅ Null handling untuk unknown exercises
|
||||
|
||||
**Debug Use**:
|
||||
|
||||
```typescript
|
||||
// Add this untuk see feature extraction
|
||||
const features = har.extractFeatures(mockLandmarks);
|
||||
console.log("Extracted Features:", features.length); // Should be 141
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **5. `test_rehab_core.ts`** 💪
|
||||
|
||||
**Path**: `/scripts/test_rehab_core.ts`
|
||||
|
||||
**Fungsi**: Unit test untuk `RehabCore` exercise recognition system
|
||||
|
||||
**Size**: ~1.1 KB
|
||||
|
||||
**Use Case**:
|
||||
|
||||
- Test all 7 exercise configs load correctly
|
||||
- Verify FSM initialization
|
||||
- Debug rep counting issues
|
||||
- Ensure config keys match
|
||||
|
||||
**Code**:
|
||||
|
||||
```typescript
|
||||
import { RehabCore } from "../lib/pose/RehabCore";
|
||||
import { Landmark } from "../lib/pose/ExerciseRules";
|
||||
|
||||
const core = new RehabCore();
|
||||
const mockLandmarks: Landmark[] = Array(33).fill({
|
||||
x: 0.5,
|
||||
y: 0.5,
|
||||
z: 0,
|
||||
visibility: 1,
|
||||
});
|
||||
|
||||
const exercises = [
|
||||
"bicep_curls",
|
||||
"hammer_curls",
|
||||
"shoulder_press",
|
||||
"lateral_raises",
|
||||
"squats",
|
||||
"deadlifts",
|
||||
"lunges",
|
||||
];
|
||||
|
||||
console.log("Testing RehabCore Config Loading...");
|
||||
|
||||
exercises.forEach((name) => {
|
||||
try {
|
||||
const result = core.process(name, mockLandmarks);
|
||||
|
||||
if (result) {
|
||||
console.log(`[PASS] ${name} -> Processed successfully.`);
|
||||
} else {
|
||||
console.error(`[FAIL] ${name} -> Returned null (Config not found?).`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`[FAIL] ${name} -> Exception:`, e);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
**How to Run**:
|
||||
|
||||
```bash
|
||||
npx tsx scripts/test_rehab_core.ts
|
||||
```
|
||||
|
||||
**Expected Output**:
|
||||
|
||||
```
|
||||
Testing RehabCore Config Loading...
|
||||
[PASS] bicep_curls -> Processed successfully.
|
||||
[PASS] hammer_curls -> Processed successfully.
|
||||
[PASS] shoulder_press -> Processed successfully.
|
||||
[PASS] lateral_raises -> Processed successfully.
|
||||
[PASS] squats -> Processed successfully.
|
||||
[PASS] deadlifts -> Processed successfully.
|
||||
[PASS] lunges -> Processed successfully.
|
||||
```
|
||||
|
||||
**What It Tests**:
|
||||
|
||||
- ✅ Exercise name normalization
|
||||
- ✅ Config lookup dari `EXERCISE_CONFIGS`
|
||||
- ✅ FSM initialization (`COUNTER_MAP`)
|
||||
- ✅ Process loop doesn't crash
|
||||
|
||||
**Common Failures**:
|
||||
|
||||
```
|
||||
[FAIL] bicep_curls -> Returned null (Config not found?)
|
||||
```
|
||||
|
||||
**Cause**: Typo in exercise name or config key
|
||||
|
||||
**Fix**: Check `ExerciseRules.ts` dan `RehabCore.ts` COUNTER_MAP
|
||||
|
||||
---
|
||||
|
||||
## 🔧 How to Run Scripts
|
||||
|
||||
### **Prerequisites**:
|
||||
|
||||
```bash
|
||||
# Install tsx (TypeScript executor)
|
||||
npm install -g tsx
|
||||
|
||||
# Or use npx (no global install)
|
||||
npx tsx scripts/[script-name].ts
|
||||
```
|
||||
|
||||
### **Environment Setup**:
|
||||
|
||||
All scripts load `.env` file:
|
||||
|
||||
```bash
|
||||
# .env
|
||||
DATABASE_URL="postgresql://user:password@localhost:5432/dbname"
|
||||
```
|
||||
|
||||
### **Package Scripts** (Optional):
|
||||
|
||||
Add to `package.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"check:links": "tsx scripts/check-links.ts",
|
||||
"check:logs": "tsx scripts/check_logs.ts",
|
||||
"seed:log": "tsx scripts/seed_log.ts",
|
||||
"test:har": "tsx scripts/test_har_core.ts",
|
||||
"test:rehab": "tsx scripts/test_rehab_core.ts"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then run:
|
||||
|
||||
```bash
|
||||
npm run check:links
|
||||
npm run test:har
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 Script Usage Matrix
|
||||
|
||||
| Script | Purpose | When to Use | Dependencies |
|
||||
| -------------------- | -------------------------- | -------------------------------- | --------------------------- |
|
||||
| `check-links.ts` | List users & relationships | After seeding, debug assignments | Prisma, DB |
|
||||
| `check_logs.ts` | View recent activity logs | After training sessions | Prisma, DB |
|
||||
| `seed_log.ts` | Create test log | UI development, testing | Prisma, DB |
|
||||
| `test_har_core.ts` | Test activity recognition | After HAR changes, debug | HARCore, XGBoost model |
|
||||
| `test_rehab_core.ts` | Test exercise recognition | After config changes, debug | RehabCore, EXERCISE_CONFIGS |
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Common Workflows
|
||||
|
||||
### **Workflow 1: Fresh Database Setup**
|
||||
|
||||
```bash
|
||||
# 1. Reset database
|
||||
npx prisma migrate reset
|
||||
|
||||
# 2. Verify users created
|
||||
npx tsx scripts/check-links.ts
|
||||
|
||||
# 3. Create test log
|
||||
npx tsx scripts/seed_log.ts
|
||||
|
||||
# 4. Verify log saved
|
||||
npx tsx scripts/check_logs.ts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **Workflow 2: Debug Exercise Recognition**
|
||||
|
||||
```bash
|
||||
# 1. Test all exercises load
|
||||
npx tsx scripts/test_rehab_core.ts
|
||||
|
||||
# 2. If FAIL, check config
|
||||
cat lib/pose/ExerciseRules.ts | grep -A 5 "bicep_curl"
|
||||
|
||||
# 3. Fix and re-test
|
||||
npx tsx scripts/test_rehab_core.ts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **Workflow 3: Monitor Production Logs**
|
||||
|
||||
```bash
|
||||
# SSH to server
|
||||
ssh user@production-server
|
||||
|
||||
# Check recent activity
|
||||
cd /path/to/app
|
||||
npx tsx scripts/check_logs.ts
|
||||
|
||||
# If fall detected, investigate
|
||||
npx tsx scripts/check_logs.ts | grep "Fall Detected"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💡 Extending Scripts
|
||||
|
||||
### **Add New Test Script**:
|
||||
|
||||
**Example**: `test_form_scoring.ts`
|
||||
|
||||
```typescript
|
||||
import { RehabCore } from "../lib/pose/RehabCore";
|
||||
import { Landmark } from "../lib/pose/ExerciseRules";
|
||||
|
||||
const core = new RehabCore();
|
||||
|
||||
// Create realistic landmarks (proper squat form)
|
||||
const goodSquatLandmarks: Landmark[] = [
|
||||
// ... 33 landmarks with correct squat angles
|
||||
];
|
||||
|
||||
const result = core.process("squat", goodSquatLandmarks);
|
||||
console.log("Form Score:", result?.scores?.deviation_mae);
|
||||
// Expected: < 8 (Excellent)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **Add Data Export Script**:
|
||||
|
||||
**Example**: `export_recaps.ts`
|
||||
|
||||
```typescript
|
||||
import { PrismaClient } from "../app/generated/client/client";
|
||||
import fs from "fs";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
const recaps = await prisma.user_recaps.findMany({
|
||||
include: { user: true, training_menus: true },
|
||||
});
|
||||
|
||||
const csv = recaps.map((r) => ({
|
||||
user: r.user?.name,
|
||||
menu: r.training_menus?.name,
|
||||
date: r.completed_at,
|
||||
// ... more fields
|
||||
}));
|
||||
|
||||
fs.writeFileSync("export.json", JSON.stringify(csv, null, 2));
|
||||
console.log("Exported to export.json");
|
||||
}
|
||||
|
||||
main();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔒 Security Notes
|
||||
|
||||
### **Safe Scripts**: ✅
|
||||
|
||||
- `check-links.ts` - Read-only
|
||||
- `check_logs.ts` - Read-only
|
||||
- `test_har_core.ts` - No DB access
|
||||
- `test_rehab_core.ts` - No DB access
|
||||
|
||||
### **Destructive Scripts**: ⚠️
|
||||
|
||||
- `seed_log.ts` - **Writes** to database
|
||||
- Safe in dev, but be careful in production
|
||||
- Consider adding `--dry-run` flag
|
||||
|
||||
### **Best Practice**:
|
||||
|
||||
```typescript
|
||||
// Add environment check
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
console.error("This script is not safe to run in production!");
|
||||
process.exit(1);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Advanced Usage
|
||||
|
||||
### **1. Continuous Testing**:
|
||||
|
||||
```bash
|
||||
# Watch mode for TDD
|
||||
npx tsx watch scripts/test_rehab_core.ts
|
||||
|
||||
# Re-runs on file change
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **2. Automated Checks**:
|
||||
|
||||
```bash
|
||||
# Add to CI/CD pipeline
|
||||
# .github/workflows/test.yml
|
||||
|
||||
- name: Run Unit Tests
|
||||
run: |
|
||||
npx tsx scripts/test_har_core.ts
|
||||
npx tsx scripts/test_rehab_core.ts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **3. Data Migration**:
|
||||
|
||||
```typescript
|
||||
// scripts/migrate_old_logs.ts
|
||||
const oldLogs = await prisma.legacy_logs.findMany();
|
||||
|
||||
for (const log of oldLogs) {
|
||||
await prisma.activity_logs.create({
|
||||
data: {
|
||||
user_id: log.userId,
|
||||
status: transformStatus(log.oldStatus),
|
||||
// ... transform logic
|
||||
},
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Troubleshooting
|
||||
|
||||
### **Issue**: `Cannot find module 'dotenv'`
|
||||
|
||||
```bash
|
||||
Error: Cannot find module 'dotenv'
|
||||
```
|
||||
|
||||
**Solution**:
|
||||
|
||||
```bash
|
||||
npm install dotenv
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **Issue**: `tsx: command not found`
|
||||
|
||||
```bash
|
||||
bash: tsx: command not found
|
||||
```
|
||||
|
||||
**Solution**:
|
||||
|
||||
```bash
|
||||
# Use npx instead
|
||||
npx tsx scripts/check-links.ts
|
||||
|
||||
# Or install globally
|
||||
npm install -g tsx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **Issue**: Prisma connection error
|
||||
|
||||
```bash
|
||||
Error: Can't reach database server
|
||||
```
|
||||
|
||||
**Solution**:
|
||||
|
||||
1. Check PostgreSQL running
|
||||
2. Verify `DATABASE_URL` in `.env`
|
||||
3. Test connection:
|
||||
```bash
|
||||
npx prisma db pull
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **Issue**: Scripts hang (promises not resolving)
|
||||
|
||||
```typescript
|
||||
// Add timeout
|
||||
setTimeout(() => process.exit(0), 5000);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 References
|
||||
|
||||
**Tools Used**:
|
||||
|
||||
- [tsx](https://github.com/esbuild-kit/tsx) - TypeScript executor
|
||||
- [Prisma Client](https://www.prisma.io/docs/concepts/components/prisma-client) - Database ORM
|
||||
- [dotenv](https://github.com/motdotla/dotenv) - Environment variables
|
||||
|
||||
**Related Docs**:
|
||||
|
||||
- `/lib/pose/` - Core AI modules
|
||||
- `/prisma/` - Database schema
|
||||
- `package.json` - NPM scripts
|
||||
|
||||
---
|
||||
|
||||
## ✅ Summary
|
||||
|
||||
**Total Scripts**: 5
|
||||
|
||||
**Categories**:
|
||||
|
||||
- 🗄️ **Database**: 3 scripts (check-links, check_logs, seed_log)
|
||||
- 🧪 **Testing**: 2 scripts (test_har_core, test_rehab_core)
|
||||
|
||||
**When to Use**:
|
||||
|
||||
- ✅ After database migrations
|
||||
- ✅ After modifying exercise configs
|
||||
- ✅ Debugging recognition issues
|
||||
- ✅ Verifying production data
|
||||
|
||||
**Quick Commands**:
|
||||
|
||||
```bash
|
||||
# Database health check
|
||||
npx tsx scripts/check-links.ts
|
||||
|
||||
# View recent activity
|
||||
npx tsx scripts/check_logs.ts
|
||||
|
||||
# Test AI systems
|
||||
npx tsx scripts/test_har_core.ts
|
||||
npx tsx scripts/test_rehab_core.ts
|
||||
```
|
||||
|
||||
Scripts ini adalah **development tools penting** untuk maintain code quality dan debug issues! 🛠️
|
||||
36
README.md
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
||||
40
app/api/coach/link-client/route.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
|
||||
import { NextResponse } from 'next/server';
|
||||
import { prisma } from '@/lib/prisma';
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { coachId, clientId } = body;
|
||||
|
||||
if (!coachId || !clientId) {
|
||||
return NextResponse.json({ error: 'Coach ID and Client ID are required' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Validate Coach
|
||||
const coach = await prisma.users.findUnique({ where: { id: String(coachId) } });
|
||||
if (!coach || coach.role !== 'COACH') {
|
||||
return NextResponse.json({ error: 'Invalid Coach ID' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Validate Client
|
||||
const client = await prisma.users.findUnique({ where: { id: String(clientId) } });
|
||||
if (!client) { // Allow taking over any user as long as they exist? Ideally check if they are CLIENT role.
|
||||
return NextResponse.json({ error: 'Client not found' }, { status: 404 });
|
||||
}
|
||||
if (client.role !== 'CLIENT') {
|
||||
return NextResponse.json({ error: 'Target user is not a Client' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Update Link
|
||||
const updatedClient = await prisma.users.update({
|
||||
where: { id: String(clientId) },
|
||||
data: { coach_id: String(coachId) }
|
||||
});
|
||||
|
||||
return NextResponse.json(updatedClient);
|
||||
} catch (error) {
|
||||
console.error("Link Client Error:", error);
|
||||
return NextResponse.json({ error: 'Failed to link client' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
48
app/api/logs/route.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
import { prisma } from '@/lib/prisma';
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const userIdHeader = request.headers.get('x-user-id');
|
||||
if (!userIdHeader) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { status, confidence, details } = body; // Confidence as string "0.95" etc
|
||||
|
||||
const log = await prisma.activity_logs.create({
|
||||
data: {
|
||||
user_id: userIdHeader,
|
||||
timestamp: new Date(),
|
||||
status: status || 'Unknown',
|
||||
confidence: String(confidence),
|
||||
details: details || {}
|
||||
}
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true, id: log.id });
|
||||
} catch (error) {
|
||||
console.error("Log Error:", error);
|
||||
return NextResponse.json({ error: 'Failed to log' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const userIdHeader = request.headers.get('x-user-id');
|
||||
if (!userIdHeader) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const logs = await prisma.activity_logs.findMany({
|
||||
where: { user_id: userIdHeader },
|
||||
orderBy: { timestamp: 'desc' },
|
||||
take: 20
|
||||
});
|
||||
|
||||
return NextResponse.json({ logs });
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: 'Failed to fetch' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
29
app/api/menus/[id]/route.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
|
||||
import { NextResponse } from 'next/server';
|
||||
import { prisma } from '@/lib/prisma';
|
||||
|
||||
export async function GET(
|
||||
request: Request,
|
||||
props: { params: Promise<{ id: string }> } // Change to Promise type
|
||||
) {
|
||||
try {
|
||||
const params = await props.params; // Await the params
|
||||
const id = parseInt(params.id);
|
||||
if (isNaN(id)) {
|
||||
return NextResponse.json({ error: 'Invalid ID' }, { status: 400 });
|
||||
}
|
||||
|
||||
const menu = await prisma.training_menus.findUnique({
|
||||
where: { id: id }
|
||||
});
|
||||
|
||||
if (!menu) {
|
||||
return NextResponse.json({ error: 'Menu not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json(menu);
|
||||
} catch (error) {
|
||||
console.error("GET Menu Detail Error:", error);
|
||||
return NextResponse.json({ error: 'Failed to fetch menu' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
58
app/api/menus/route.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
|
||||
import { NextResponse } from 'next/server';
|
||||
import { prisma } from '@/lib/prisma';
|
||||
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const userIdHeader = request.headers.get('x-user-id');
|
||||
const userId = userIdHeader || null; // String ID
|
||||
|
||||
let whereClause = {};
|
||||
|
||||
if (userId) {
|
||||
const user = await prisma.users.findUnique({ where: { id: userId } });
|
||||
if (user?.role === 'COACH') {
|
||||
whereClause = { author_id: userId };
|
||||
} else if (user?.role === 'CLIENT') {
|
||||
whereClause = { client_id: userId }; // Only see assigned menus
|
||||
}
|
||||
}
|
||||
|
||||
const menus = await prisma.training_menus.findMany({
|
||||
where: whereClause,
|
||||
include: {
|
||||
assigned_client: {
|
||||
select: { name: true, id: true }
|
||||
}
|
||||
},
|
||||
orderBy: { created_at: 'desc' }
|
||||
});
|
||||
return NextResponse.json(menus);
|
||||
} catch (error) {
|
||||
console.error("GET Error:", error);
|
||||
return NextResponse.json({ error: 'Failed to fetch menus' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const userIdHeader = request.headers.get('x-user-id');
|
||||
const authorId = userIdHeader || null; // String ID
|
||||
|
||||
const body = await request.json();
|
||||
const { name, exercises, client_id } = body;
|
||||
const newMenu = await prisma.training_menus.create({
|
||||
data: {
|
||||
name,
|
||||
exercises: exercises,
|
||||
created_at: new Date(),
|
||||
author_id: authorId,
|
||||
client_id: client_id || null // Save assigned client
|
||||
}
|
||||
});
|
||||
return NextResponse.json(newMenu);
|
||||
} catch (error) {
|
||||
console.error("POST Error:", error);
|
||||
return NextResponse.json({ error: 'Failed to create menu' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
32
app/api/recap/[id]/route.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
|
||||
import { NextResponse } from 'next/server';
|
||||
import { prisma } from '@/lib/prisma';
|
||||
|
||||
export async function GET(
|
||||
request: Request,
|
||||
props: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const params = await props.params;
|
||||
const id = parseInt(params.id);
|
||||
if (isNaN(id)) {
|
||||
return NextResponse.json({ error: 'Invalid ID' }, { status: 400 });
|
||||
}
|
||||
|
||||
const recap = await prisma.user_recaps.findUnique({
|
||||
where: { id: id },
|
||||
include: {
|
||||
training_menus: true // Include menu details
|
||||
}
|
||||
});
|
||||
|
||||
if (!recap) {
|
||||
return NextResponse.json({ error: 'Recap not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json(recap);
|
||||
} catch (error) {
|
||||
console.error("GET Recap Detail Error:", error);
|
||||
return NextResponse.json({ error: 'Failed to fetch recap' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
62
app/api/recap/route.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
|
||||
import { NextResponse } from 'next/server';
|
||||
import { prisma } from '@/lib/prisma';
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { menu_id, user_id, summary } = body;
|
||||
|
||||
const recap = await prisma.user_recaps.create({
|
||||
data: {
|
||||
menu_id: Number(menu_id), // Menu ID stays Int
|
||||
user_id: user_id ? String(user_id) : null,
|
||||
summary: summary,
|
||||
completed_at: new Date()
|
||||
}
|
||||
});
|
||||
|
||||
return NextResponse.json(recap);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return NextResponse.json({ error: 'Failed to save recap' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const userIdHeader = request.headers.get('x-user-id');
|
||||
const userId = userIdHeader || null; // String ID
|
||||
|
||||
let whereClause = {};
|
||||
|
||||
if (userId) {
|
||||
const user = await prisma.users.findUnique({
|
||||
where: { id: userId },
|
||||
include: { clients: true }
|
||||
});
|
||||
|
||||
if (user?.role === 'COACH') {
|
||||
// Coach sees recaps from their clients
|
||||
const clientIds = user.clients.map(c => c.id);
|
||||
whereClause = { user_id: { in: clientIds } };
|
||||
} else if (user?.role === 'CLIENT') {
|
||||
// Client sees only their own recaps
|
||||
whereClause = { user_id: userId };
|
||||
}
|
||||
}
|
||||
|
||||
const recaps = await prisma.user_recaps.findMany({
|
||||
where: whereClause,
|
||||
take: 50,
|
||||
include: {
|
||||
user: { select: { name: true, id: true } },
|
||||
training_menus: { select: { name: true, id: true } }
|
||||
},
|
||||
orderBy: { completed_at: 'desc' }
|
||||
});
|
||||
return NextResponse.json(recaps);
|
||||
} catch (error) {
|
||||
console.error("GET Recap Error:", error);
|
||||
return NextResponse.json({ error: 'Failed to fetch recaps' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
45
app/api/register/route.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
|
||||
import { NextResponse } from 'next/server';
|
||||
import { prisma } from '@/lib/prisma';
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { name, role } = body;
|
||||
|
||||
if (!name || !role || !['COACH', 'CLIENT'].includes(role)) {
|
||||
return NextResponse.json({ error: 'Invalid name or role' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Generate 6-char random ID
|
||||
const generateId = () => {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*';
|
||||
let result = '';
|
||||
for (let i = 0; i < 6; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
let uniqueId = generateId();
|
||||
let exists = await prisma.users.findUnique({ where: { id: uniqueId } });
|
||||
while (exists) {
|
||||
uniqueId = generateId();
|
||||
exists = await prisma.users.findUnique({ where: { id: uniqueId } });
|
||||
}
|
||||
|
||||
const newUser = await prisma.users.create({
|
||||
data: {
|
||||
id: uniqueId,
|
||||
name,
|
||||
role,
|
||||
created_at: new Date()
|
||||
}
|
||||
});
|
||||
|
||||
return NextResponse.json(newUser);
|
||||
} catch (error) {
|
||||
console.error("Register Error:", error);
|
||||
return NextResponse.json({ error: 'Failed to register user' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
26
app/api/users/[id]/route.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
|
||||
import { NextResponse } from 'next/server';
|
||||
import { prisma } from '@/lib/prisma';
|
||||
|
||||
export async function GET(
|
||||
request: Request,
|
||||
props: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const params = await props.params;
|
||||
const id = params.id; // String ID
|
||||
|
||||
const user = await prisma.users.findUnique({
|
||||
where: { id: id },
|
||||
include: { coach: true }
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json(user);
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: 'Internal Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
30
app/api/users/route.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
|
||||
import { NextResponse } from 'next/server';
|
||||
import { prisma } from '@/lib/prisma';
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const coachId = searchParams.get('coachId');
|
||||
|
||||
try {
|
||||
let whereClause = {};
|
||||
|
||||
if (coachId) {
|
||||
whereClause = { coach_id: coachId };
|
||||
} else {
|
||||
// Default? user search? For now just return empty or all clients?
|
||||
// Let's restrict to only returning if coachId is provided for safety/relevance context
|
||||
return NextResponse.json([]);
|
||||
}
|
||||
|
||||
const users = await prisma.users.findMany({
|
||||
where: whereClause,
|
||||
orderBy: { name: 'asc' }
|
||||
});
|
||||
|
||||
return NextResponse.json(users);
|
||||
} catch (error) {
|
||||
console.error("GET Users Error:", error);
|
||||
return NextResponse.json({ error: 'Failed to fetch users' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
289
app/client/free/page.tsx
Normal file
|
|
@ -0,0 +1,289 @@
|
|||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { Plus, Trash2, PlayCircle, ArrowLeft, Copy } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
|
||||
interface ExerciseItem {
|
||||
id: string;
|
||||
name: string;
|
||||
reps: number;
|
||||
weight: number;
|
||||
rest_time_seconds: number;
|
||||
}
|
||||
|
||||
interface RoundData {
|
||||
id: string;
|
||||
exercises: ExerciseItem[];
|
||||
}
|
||||
|
||||
export default function FreeModeBuilder() {
|
||||
const router = useRouter();
|
||||
|
||||
// --- Round-Based State ---
|
||||
const [rounds, setRounds] = useState<RoundData[]>([
|
||||
{
|
||||
id: 'round-1',
|
||||
exercises: [
|
||||
{ id: 'ex-1', name: 'Squat', reps: 10, weight: 20, rest_time_seconds: 30 }
|
||||
]
|
||||
}
|
||||
]);
|
||||
|
||||
// --- Actions ---
|
||||
|
||||
const addRound = () => {
|
||||
setRounds([...rounds, {
|
||||
id: Math.random().toString(36).substr(2, 9),
|
||||
exercises: []
|
||||
}]);
|
||||
};
|
||||
|
||||
const duplicateRound = (sourceIndex: number) => {
|
||||
const source = rounds[sourceIndex];
|
||||
const newExercises = source.exercises.map(ex => ({
|
||||
...ex,
|
||||
id: Math.random().toString(36).substr(2, 9)
|
||||
}));
|
||||
|
||||
// Insert after the source round
|
||||
const newRounds = [...rounds];
|
||||
newRounds.splice(sourceIndex + 1, 0, {
|
||||
id: Math.random().toString(36).substr(2, 9),
|
||||
exercises: newExercises
|
||||
});
|
||||
setRounds(newRounds);
|
||||
};
|
||||
|
||||
const removeRound = (index: number) => {
|
||||
setRounds(rounds.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
const addExerciseToRound = (roundIndex: number) => {
|
||||
const newRounds = [...rounds];
|
||||
newRounds[roundIndex].exercises.push({
|
||||
id: Math.random().toString(36).substr(2, 9),
|
||||
name: 'Squat',
|
||||
reps: 10,
|
||||
weight: 10,
|
||||
rest_time_seconds: 30
|
||||
});
|
||||
setRounds(newRounds);
|
||||
};
|
||||
|
||||
const removeExerciseFromRound = (roundIndex: number, exIndex: number) => {
|
||||
const newRounds = [...rounds];
|
||||
newRounds[roundIndex].exercises = newRounds[roundIndex].exercises.filter((_, i) => i !== exIndex);
|
||||
setRounds(newRounds);
|
||||
};
|
||||
|
||||
const updateExercise = (roundIndex: number, exIndex: number, field: keyof ExerciseItem, value: any) => {
|
||||
const newRounds = [...rounds];
|
||||
newRounds[roundIndex].exercises[exIndex] = {
|
||||
...newRounds[roundIndex].exercises[exIndex],
|
||||
[field]: value
|
||||
};
|
||||
setRounds(newRounds);
|
||||
};
|
||||
|
||||
const startTraining = () => {
|
||||
if (rounds.length === 0) return;
|
||||
if (rounds.every(r => r.exercises.length === 0)) return;
|
||||
|
||||
// Flatten Logic: Expand Rounds into Linear List
|
||||
// Matches Coach App logic exactly
|
||||
const flatList: any[] = [];
|
||||
const counts: Record<string, number> = {};
|
||||
const totals: Record<string, number> = {};
|
||||
|
||||
// 1. Calculate Totals (First Pass)
|
||||
rounds.forEach(round => {
|
||||
round.exercises.forEach(ex => {
|
||||
totals[ex.name] = (totals[ex.name] || 0) + 1;
|
||||
});
|
||||
});
|
||||
|
||||
// 2. Flatten and Assign Indices
|
||||
rounds.forEach((round) => {
|
||||
round.exercises.forEach(ex => {
|
||||
counts[ex.name] = (counts[ex.name] || 0) + 1;
|
||||
|
||||
flatList.push({
|
||||
name: ex.name,
|
||||
reps: ex.reps,
|
||||
weight: ex.weight,
|
||||
rest_time_seconds: ex.rest_time_seconds,
|
||||
set_index: counts[ex.name],
|
||||
total_sets: totals[ex.name]
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Save to LocalStorage
|
||||
const freeMenu = {
|
||||
id: 'free-mode',
|
||||
name: 'Free Session',
|
||||
exercises: flatList
|
||||
};
|
||||
localStorage.setItem('straps_free_mode_menu', JSON.stringify(freeMenu));
|
||||
|
||||
// Redirect
|
||||
router.push('/client/training?mode=free');
|
||||
};
|
||||
|
||||
const EXERCISE_OPTIONS = [
|
||||
"Bicep Curl", "Hammer Curl", "Squat", "Deadlift", "Lunges", "Overhead Press", "Lateral Raises"
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background text-foreground p-4 md:p-8 font-sans pb-32">
|
||||
<header className="max-w-3xl mx-auto mb-10 flex items-center gap-4">
|
||||
<Link href="/client" className="p-3 bg-white hover:bg-zinc-100 rounded-full transition-colors border border-zinc-200 shadow-sm">
|
||||
<ArrowLeft className="w-5 h-5 text-zinc-600" />
|
||||
</Link>
|
||||
<div>
|
||||
<h1 className="text-2xl md:text-3xl font-light text-zinc-900 tracking-wide">
|
||||
Free Style <span className="font-bold text-primary">Composer</span>
|
||||
</h1>
|
||||
<p className="text-zinc-500 text-sm mt-1">Design training blocks set-by-set.</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="max-w-3xl mx-auto space-y-8">
|
||||
<AnimatePresence>
|
||||
{rounds.map((round, roundIndex) => (
|
||||
<motion.div
|
||||
key={round.id}
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
className="bg-zinc-50 border border-zinc-200 rounded-3xl p-6 md:p-8 relative shadow-sm group/round"
|
||||
>
|
||||
{/* Round Header */}
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h3 className="text-xl font-black text-zinc-300 uppercase tracking-tighter flex items-center gap-2">
|
||||
<span className="text-4xl text-zinc-200">#{ (roundIndex + 1).toString().padStart(2, '0') }</span>
|
||||
SET/GROUP
|
||||
</h3>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => duplicateRound(roundIndex)}
|
||||
className="p-2 text-zinc-400 hover:text-blue-500 hover:bg-blue-50 rounded-xl transition-all"
|
||||
title="Duplicate this Round"
|
||||
>
|
||||
<Copy className="w-5 h-5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeRound(roundIndex)}
|
||||
disabled={rounds.length === 1}
|
||||
className="p-2 text-zinc-300 hover:text-red-500 hover:bg-red-50 rounded-xl transition-all disabled:opacity-0"
|
||||
>
|
||||
<Trash2 className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Exercises List */}
|
||||
<div className="space-y-4">
|
||||
{round.exercises.map((ex, exIndex) => (
|
||||
<div key={ex.id} className="bg-white p-4 rounded-xl shadow-sm border border-zinc-100 grid grid-cols-2 md:grid-cols-4 gap-4 items-center group/ex relative">
|
||||
|
||||
{/* Name */}
|
||||
<div className="col-span-2 md:col-span-1">
|
||||
<label className="block text-[10px] font-bold text-zinc-300 uppercase mb-1">Exercise</label>
|
||||
<div className="relative">
|
||||
<select
|
||||
value={ex.name}
|
||||
onChange={(e) => updateExercise(roundIndex, exIndex, 'name', e.target.value)}
|
||||
className="w-full bg-zinc-50 border border-zinc-100 rounded-lg py-2 px-2 font-bold text-zinc-900 focus:outline-none appearance-none cursor-pointer"
|
||||
>
|
||||
{EXERCISE_OPTIONS.map(opt => <option key={opt} value={opt}>{opt}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Kg */}
|
||||
<div>
|
||||
<label className="block text-[10px] font-bold text-zinc-300 uppercase mb-1 text-center">Kg</label>
|
||||
<input
|
||||
type="number"
|
||||
value={isNaN(ex.weight) ? '' : ex.weight}
|
||||
onChange={(e) => updateExercise(roundIndex, exIndex, 'weight', parseFloat(e.target.value))}
|
||||
className="w-full bg-zinc-50 border border-zinc-100 rounded-lg px-2 py-2 text-center font-mono text-sm focus:border-primary outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Reps */}
|
||||
<div>
|
||||
<label className="block text-[10px] font-bold text-zinc-300 uppercase mb-1 text-center">Reps</label>
|
||||
<input
|
||||
type="number"
|
||||
value={isNaN(ex.reps) ? '' : ex.reps}
|
||||
onChange={(e) => updateExercise(roundIndex, exIndex, 'reps', parseInt(e.target.value))}
|
||||
className="w-full bg-zinc-50 border border-zinc-100 rounded-lg px-2 py-2 text-center font-mono text-sm focus:border-primary outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Rest */}
|
||||
<div>
|
||||
<label className="block text-[10px] font-bold text-zinc-300 uppercase mb-1 text-center">Rest(s)</label>
|
||||
<input
|
||||
type="number"
|
||||
value={isNaN(ex.rest_time_seconds) ? '' : ex.rest_time_seconds}
|
||||
onChange={(e) => updateExercise(roundIndex, exIndex, 'rest_time_seconds', parseFloat(e.target.value))}
|
||||
className="w-full bg-zinc-50 border border-zinc-100 rounded-lg px-2 py-2 text-center font-mono text-sm focus:border-primary outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Remove Exercise */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeExerciseFromRound(roundIndex, exIndex)}
|
||||
className="absolute -top-2 -right-2 p-1.5 bg-white border border-zinc-100 text-zinc-300 hover:text-red-500 rounded-full shadow-sm opacity-0 group-hover/ex:opacity-100 transition-opacity"
|
||||
>
|
||||
<Trash2 className="w-3 h-3" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => addExerciseToRound(roundIndex)}
|
||||
className="w-full py-3 border border-dashed border-zinc-300 rounded-xl text-zinc-400 text-sm font-bold hover:text-primary hover:border-primary hover:bg-white transition-all flex items-center justify-center gap-2"
|
||||
>
|
||||
<Plus className="w-4 h-4" /> Add Exercise
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
|
||||
<div className="flex gap-4 pt-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={addRound}
|
||||
className="flex-1 bg-white border border-dashed border-zinc-300 text-zinc-500 hover:text-primary hover:border-primary p-6 rounded-xl flex items-center justify-center gap-2 font-bold transition-all hover:bg-zinc-50 shadow-sm"
|
||||
>
|
||||
<Plus className="w-5 h-5" /> Add New Round
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="fixed bottom-0 left-0 right-0 p-6 bg-white/80 backdrop-blur-lg border-t border-zinc-200 flex justify-center z-50">
|
||||
<button
|
||||
type="button"
|
||||
onClick={startTraining}
|
||||
disabled={rounds.length === 0}
|
||||
className="w-full max-w-md bg-zinc-900 hover:bg-black text-white font-black uppercase tracking-widest py-4 rounded-2xl shadow-xl transform transition-all active:scale-95 disabled:opacity-50 flex items-center justify-center gap-3"
|
||||
>
|
||||
<PlayCircle className="w-6 h-6" /> START SESSION
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
459
app/client/monitor/page.tsx
Normal file
|
|
@ -0,0 +1,459 @@
|
|||
'use client';
|
||||
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { HARCore } from '@/lib/pose/HARCore';
|
||||
// Import from the official tasks-vision package
|
||||
import { PoseLandmarker, FilesetResolver, DrawingUtils } from '@mediapipe/tasks-vision';
|
||||
import { ArrowLeft, Activity, ShieldAlert, Ban, CheckCircle, Edit3, Trash2, MousePointerClick, BellRing } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
|
||||
import { AuthProvider, useAuth } from '@/lib/auth';
|
||||
|
||||
export default function MonitorPageWrap() {
|
||||
return (
|
||||
<AuthProvider>
|
||||
<MonitorPage />
|
||||
</AuthProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function MonitorPage() {
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
// State
|
||||
const [stats, setStats] = useState({ status: 'Initializing...', confidence: 0 });
|
||||
|
||||
// Safety Zone State (Normalized 0-1)
|
||||
const [safetyZone, setSafetyZone] = useState<{x: number, y: number, w: number, h: number} | null>(null);
|
||||
const [isEditingZone, setIsEditingZone] = useState(false);
|
||||
const [isDrawing, setIsDrawing] = useState(false);
|
||||
const [startPoint, setStartPoint] = useState<{x: number, y: number} | null>(null);
|
||||
const zoneRef = useRef<{x: number, y: number, w: number, h: number} | null>(null);
|
||||
|
||||
// Alarm State
|
||||
const [alarmTriggered, setAlarmTriggered] = useState(false);
|
||||
const fallStartTimeRef = useRef<number | null>(null);
|
||||
const [timeToAlarm, setTimeToAlarm] = useState<number | null>(null);
|
||||
|
||||
// Sync ref for loop access
|
||||
useEffect(() => {
|
||||
zoneRef.current = safetyZone;
|
||||
}, [safetyZone]);
|
||||
|
||||
// Refs
|
||||
const harRef = useRef<HARCore | null>(null);
|
||||
const landmarkerRef = useRef<PoseLandmarker | null>(null);
|
||||
const requestRef = useRef<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
|
||||
async function init() {
|
||||
try {
|
||||
// 1. Init Core
|
||||
const core = new HARCore();
|
||||
harRef.current = core;
|
||||
|
||||
// 2. Init Vision
|
||||
const vision = await FilesetResolver.forVisionTasks(
|
||||
"https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.14/wasm"
|
||||
);
|
||||
if (!isMounted) return;
|
||||
|
||||
const landmarker = await PoseLandmarker.createFromOptions(vision, {
|
||||
baseOptions: {
|
||||
modelAssetPath: `https://storage.googleapis.com/mediapipe-models/pose_landmarker/pose_landmarker_full/float16/1/pose_landmarker_full.task`,
|
||||
delegate: "GPU"
|
||||
},
|
||||
runningMode: "VIDEO",
|
||||
numPoses: 1
|
||||
});
|
||||
landmarkerRef.current = landmarker;
|
||||
|
||||
// 3. Init Camera
|
||||
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({
|
||||
video: { width: 640, height: 480 }
|
||||
});
|
||||
|
||||
if (videoRef.current) {
|
||||
videoRef.current.srcObject = stream;
|
||||
await videoRef.current.play();
|
||||
|
||||
setIsLoading(false);
|
||||
requestRef.current = requestAnimationFrame(predictWebcam);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.error("Init Error:", e);
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
init();
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
if (requestRef.current) cancelAnimationFrame(requestRef.current);
|
||||
if (videoRef.current && videoRef.current.srcObject) {
|
||||
(videoRef.current.srcObject as MediaStream).getTracks().forEach(t => t.stop());
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Logging
|
||||
// Logging
|
||||
const { user } = useAuth();
|
||||
const userRef = useRef(user);
|
||||
|
||||
// Keep userRef synced
|
||||
useEffect(() => {
|
||||
userRef.current = user;
|
||||
}, [user]);
|
||||
|
||||
const lastLogRef = useRef(Date.now());
|
||||
const alarmLoggedRef = useRef(false);
|
||||
|
||||
const sendLog = async (data: any) => {
|
||||
if (!userRef.current) return;
|
||||
try {
|
||||
await fetch('/api/logs', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-user-id': userRef.current.id
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
} catch (e) { console.error("Log failed", e); }
|
||||
};
|
||||
|
||||
// Loop
|
||||
const lastVideoTimeRef = useRef(-1);
|
||||
|
||||
const predictWebcam = async () => {
|
||||
const video = videoRef.current;
|
||||
const canvas = canvasRef.current;
|
||||
const landmarker = landmarkerRef.current;
|
||||
const har = harRef.current;
|
||||
|
||||
if (video && canvas && landmarker && har) {
|
||||
let startTimeMs = performance.now();
|
||||
|
||||
if (lastVideoTimeRef.current !== video.currentTime && video.videoWidth > 0 && video.videoHeight > 0) {
|
||||
lastVideoTimeRef.current = video.currentTime;
|
||||
|
||||
const result = landmarker.detectForVideo(video, startTimeMs);
|
||||
|
||||
// Draw
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (ctx) {
|
||||
ctx.save();
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// Mirror
|
||||
ctx.scale(-1, 1);
|
||||
ctx.translate(-canvas.width, 0);
|
||||
|
||||
// Draw Video Frame
|
||||
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
||||
|
||||
if (result.landmarks) {
|
||||
const drawingUtils = new DrawingUtils(ctx);
|
||||
for (const lm of result.landmarks) {
|
||||
drawingUtils.drawLandmarks(lm, { radius: 1, color: '#00FF00' });
|
||||
drawingUtils.drawConnectors(lm, PoseLandmarker.POSE_CONNECTIONS, { color: '#00FF00', lineWidth: 2 });
|
||||
}
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// Process Logic
|
||||
if (result.landmarks && result.landmarks.length > 0) {
|
||||
const lm = result.landmarks[0];
|
||||
|
||||
// 1. Run HAR first (Always detect status)
|
||||
const res = await har.process(lm as any);
|
||||
|
||||
if (res) {
|
||||
// 2. Check Safety Zone
|
||||
let isUnsafe = false;
|
||||
if (zoneRef.current) {
|
||||
const z = zoneRef.current;
|
||||
const inZone = (p: {x:number, y:number}) =>
|
||||
p.x >= z.x && p.x <= (z.x + z.w) &&
|
||||
p.y >= z.y && p.y <= (z.y + z.h);
|
||||
|
||||
let outsideCount = 0;
|
||||
for (const point of lm) {
|
||||
// Convert to Screen Coords (Mirrored)
|
||||
const screenPoint = { x: 1 - point.x, y: point.y };
|
||||
if (!inZone(screenPoint)) {
|
||||
outsideCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Threshold: > 70% of points outside triggers Unsafe
|
||||
if ((outsideCount / lm.length) > 0.7) {
|
||||
isUnsafe = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Update Status
|
||||
setStats({
|
||||
status: res.status,
|
||||
confidence: res.confidence || 0
|
||||
});
|
||||
|
||||
// 3. Check Alarm Condition
|
||||
let currentAlarmState = false;
|
||||
if (res.status === 'Fall Detected' && isUnsafe) {
|
||||
const now = Date.now();
|
||||
if (!fallStartTimeRef.current) {
|
||||
fallStartTimeRef.current = now;
|
||||
}
|
||||
|
||||
const elapsed = now - fallStartTimeRef.current;
|
||||
if (elapsed > 10000) {
|
||||
// Trigger Alarm
|
||||
setAlarmTriggered(true);
|
||||
currentAlarmState = true;
|
||||
} else {
|
||||
// Update countdown for UI
|
||||
setTimeToAlarm(Math.ceil((10000 - elapsed) / 1000));
|
||||
}
|
||||
} else {
|
||||
// Reset
|
||||
fallStartTimeRef.current = null;
|
||||
setTimeToAlarm(null);
|
||||
}
|
||||
|
||||
// 4. Logging Logic
|
||||
const now = Date.now();
|
||||
|
||||
// A. Check for Alarm Log (Immediate)
|
||||
// If alarm just triggered (transition) or is buzzing
|
||||
if (currentAlarmState && !alarmLoggedRef.current) {
|
||||
sendLog({
|
||||
status: 'ALARM: Fall Outside Zone',
|
||||
confidence: '1.0',
|
||||
details: { reason: 'Fall detected outside safe zone > 10s' }
|
||||
});
|
||||
alarmLoggedRef.current = true; // Prevent spamming per frame
|
||||
}
|
||||
|
||||
// B. Periodic Log (Every 1 min)
|
||||
if (now - lastLogRef.current > 60000) {
|
||||
sendLog({
|
||||
status: res.status,
|
||||
confidence: String(res.confidence),
|
||||
details: { isUnsafe, zoneConfigured: !!zoneRef.current }
|
||||
});
|
||||
lastLogRef.current = now;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
requestRef.current = requestAnimationFrame(predictWebcam);
|
||||
};
|
||||
|
||||
// Drawing Handlers
|
||||
const handleMouseDown = (e: React.MouseEvent) => {
|
||||
if (!isEditingZone || !canvasRef.current) return;
|
||||
const rect = canvasRef.current.getBoundingClientRect();
|
||||
const x = (e.clientX - rect.left) / rect.width;
|
||||
const y = (e.clientY - rect.top) / rect.height;
|
||||
setIsDrawing(true);
|
||||
setStartPoint({x, y});
|
||||
setSafetyZone({x, y, w: 0, h: 0});
|
||||
};
|
||||
|
||||
const handleMouseMove = (e: React.MouseEvent) => {
|
||||
if (!isDrawing || !startPoint || !canvasRef.current) return;
|
||||
const rect = canvasRef.current.getBoundingClientRect();
|
||||
const currentX = (e.clientX - rect.left) / rect.width;
|
||||
const currentY = (e.clientY - rect.top) / rect.height;
|
||||
|
||||
const w = Math.abs(currentX - startPoint.x);
|
||||
const h = Math.abs(currentY - startPoint.y);
|
||||
const x = Math.min(currentX, startPoint.x);
|
||||
const y = Math.min(currentY, startPoint.y);
|
||||
|
||||
setSafetyZone({x, y, w, h});
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
setIsDrawing(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background text-foreground p-6 font-sans selection:bg-primary/30">
|
||||
<header className="flex justify-between items-center mb-8">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link href="/client" className="p-2 bg-white rounded-full hover:bg-zinc-100 transition-colors border border-zinc-200">
|
||||
<ArrowLeft className="w-5 h-5 text-zinc-600" />
|
||||
</Link>
|
||||
<h1 className="text-3xl font-light tracking-widest text-zinc-800">LIVE<span className="font-bold text-primary">.MONITOR</span></h1>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-zinc-400 text-xs uppercase tracking-widest">
|
||||
<Activity className="w-4 h-4 animate-pulse text-green-500" />
|
||||
System Active
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 max-w-6xl mx-auto">
|
||||
{/* Main Camera View */}
|
||||
<div className="md:col-span-3 relative border-8 border-white rounded-[2rem] overflow-hidden bg-zinc-100 shadow-2xl group">
|
||||
{isLoading && <div className="absolute inset-0 flex items-center justify-center text-blue-400 font-mono animate-pulse">Loading AI Engine (WASM)...</div>}
|
||||
<video ref={videoRef} className="hidden" width="640" height="480" autoPlay playsInline muted />
|
||||
|
||||
{/* Interaction Layer */}
|
||||
<div
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseUp={handleMouseUp}
|
||||
onMouseLeave={handleMouseUp}
|
||||
className={`absolute inset-0 z-30 ${isEditingZone ? 'cursor-crosshair' : 'cursor-default'}`}
|
||||
>
|
||||
{/* Render Safety Zone using HTML Overlay */}
|
||||
{safetyZone && (
|
||||
<div
|
||||
className="absolute border-4 duration-300 border-green-500/50 bg-green-500/10"
|
||||
style={{
|
||||
left: `${safetyZone.x * 100}%`,
|
||||
top: `${safetyZone.y * 100}%`,
|
||||
width: `${safetyZone.w * 100}%`,
|
||||
height: `${safetyZone.h * 100}%`
|
||||
}}
|
||||
>
|
||||
<div className="absolute top-0 left-0 -translate-y-full px-2 py-1 text-xs font-bold rounded-t-lg bg-green-500 text-white">
|
||||
SAFE ZONE
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<canvas ref={canvasRef} width="640" height="480" className="w-full h-auto object-contain relative z-10" />
|
||||
|
||||
{/* Status Overlay */}
|
||||
<div className="absolute top-6 left-6 flex gap-4 z-10 pointer-events-none">
|
||||
<div className={`px-6 py-3 rounded-2xl text-lg font-bold tracking-widest backdrop-blur-md border shadow-lg transition-colors duration-300 flex items-center gap-3 ${
|
||||
stats.status === 'Fall Detected'
|
||||
? 'bg-red-500/90 border-red-500 text-white animate-pulse'
|
||||
: 'bg-white/90 border-zinc-200 text-zinc-800'
|
||||
}`}>
|
||||
{stats.status === 'Fall Detected' && <Ban className="w-6 h-6" />}
|
||||
{stats.status}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Danger Countdown */}
|
||||
{timeToAlarm !== null && !alarmTriggered && (
|
||||
<div className="absolute inset-0 flex items-center justify-center z-40 bg-red-500/20 pointer-events-none">
|
||||
<div className="bg-red-600 text-white px-8 py-6 rounded-3xl animate-bounce flex flex-col items-center shadow-2xl">
|
||||
<ShieldAlert className="w-12 h-12 mb-2" />
|
||||
<div className="text-4xl font-black">{timeToAlarm}</div>
|
||||
<div className="text-xs font-bold uppercase tracking-widest">Zone Violation Detected</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ALARM TRIGGERED */}
|
||||
{alarmTriggered && (
|
||||
<div className="absolute inset-0 z-50 bg-red-600 animate-pulse flex flex-col items-center justify-center text-white p-8 text-center">
|
||||
<BellRing className="w-24 h-24 mb-6 animate-bounce" />
|
||||
<h1 className="text-6xl font-black mb-4 tracking-tighter">EMERGENCY</h1>
|
||||
<p className="text-xl font-bold uppercase tracking-widest mb-12">Fall Detected Outside Safe Zone</p>
|
||||
<button
|
||||
onClick={() => {
|
||||
setAlarmTriggered(false);
|
||||
fallStartTimeRef.current = null;
|
||||
alarmLoggedRef.current = false;
|
||||
}}
|
||||
className="bg-white text-red-600 px-10 py-4 rounded-full font-black text-xl hover:scale-105 transition-all shadow-xl uppercase border-4 border-red-800 pointer-events-auto"
|
||||
>
|
||||
DISMISS ALARM
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isEditingZone && (
|
||||
<div className="absolute bottom-6 left-0 right-0 flex justify-center z-30 pointer-events-none">
|
||||
<div className="bg-yellow-100 text-yellow-800 px-4 py-2 rounded-full text-xs font-bold uppercase tracking-wider shadow-md animate-bounce">
|
||||
Mode: Draw Safety Zone
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Sidebar Controls */}
|
||||
<div className="md:col-span-1 flex flex-col gap-4">
|
||||
<div className="bg-white p-6 rounded-2xl border border-zinc-200 shadow-sm">
|
||||
<h3 className="text-zinc-400 text-xs font-bold uppercase tracking-widest mb-4">Safety Controls</h3>
|
||||
|
||||
<div className="space-y-3">
|
||||
<button
|
||||
onClick={() => setIsEditingZone(!isEditingZone)}
|
||||
className={`w-full flex items-center gap-3 px-4 py-3 rounded-xl transition-all font-medium text-sm ${
|
||||
isEditingZone
|
||||
? 'bg-yellow-50 text-yellow-700 border border-yellow-200 shadow-inner'
|
||||
: 'bg-zinc-50 text-zinc-700 hover:bg-zinc-100 border border-zinc-100'
|
||||
}`}
|
||||
>
|
||||
<Edit3 className="w-4 h-4" />
|
||||
{isEditingZone ? 'Done Editing' : 'Edit Safe Zone'}
|
||||
</button>
|
||||
|
||||
{safetyZone && (
|
||||
<button
|
||||
onClick={() => setSafetyZone(null)}
|
||||
className="w-full flex items-center gap-3 px-4 py-3 rounded-xl bg-red-50 text-red-600 hover:bg-red-100 border border-red-100 transition-all font-medium text-sm"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" /> Clear Zone
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{safetyZone ? (
|
||||
<div className="mt-6 p-4 bg-green-50 rounded-xl border border-green-100">
|
||||
<div className="flex items-center gap-2 text-green-700 font-bold text-xs uppercase mb-1">
|
||||
<CheckCircle className="w-4 h-4" /> Zone Active
|
||||
</div>
|
||||
<p className="text-green-600 text-xs leading-relaxed">
|
||||
Alarm triggers if a fall is detected OUTSIDE this zone for > 10 seconds.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-6 p-4 bg-zinc-50 rounded-xl border border-zinc-100">
|
||||
<div className="flex items-center gap-2 text-zinc-400 font-bold text-xs uppercase mb-1">
|
||||
<MousePointerClick className="w-4 h-4" /> No Zone
|
||||
</div>
|
||||
<p className="text-zinc-400 text-xs leading-relaxed">
|
||||
Click "Edit Safe Zone" and draw a box on the camera to define the safe area.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-6 rounded-2xl border border-zinc-200 shadow-sm flex-1">
|
||||
<h3 className="text-zinc-400 text-xs font-bold uppercase tracking-widest mb-4">Stats</h3>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<div className="text-sm text-zinc-500 mb-1">Current State</div>
|
||||
<div className="text-2xl font-bold text-zinc-800">{stats.status}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-zinc-500 mb-1">AI Confidence</div>
|
||||
<div className="text-xl font-mono text-primary">{(stats.confidence * 100).toFixed(1)}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
148
app/client/page.tsx
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { PlayCircle, Eye, ArrowRight, Activity as ActivityIcon } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { useAuth, AuthProvider } from '@/lib/auth';
|
||||
|
||||
export default function ClientHubWrap() {
|
||||
return (
|
||||
<AuthProvider>
|
||||
<ClientHub />
|
||||
</AuthProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function ClientHub() {
|
||||
const { user } = useAuth();
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background text-foreground p-8 font-sans selection:bg-primary/30 flex items-center justify-center">
|
||||
<div className="max-w-5xl w-full">
|
||||
<header className="mb-16 text-center">
|
||||
<div className="flex flex-col items-center gap-2 mb-4">
|
||||
<h1 className="text-5xl font-light tracking-widest text-zinc-800">
|
||||
Hello, <span className="font-bold text-primary">{user?.name || 'Client'}</span>.
|
||||
</h1>
|
||||
<div className="flex items-center gap-4 text-sm text-zinc-400 mt-2 bg-zinc-50 px-4 py-2 rounded-full border border-zinc-100">
|
||||
<span>ID: <strong className="text-zinc-600">{user?.id}</strong></span>
|
||||
{user?.coach && (
|
||||
<>
|
||||
<span className="w-1 h-1 bg-zinc-300 rounded-full"></span>
|
||||
<span>Coach: <strong className="text-zinc-600">{user.coach.name}</strong></span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-zinc-500 text-lg">What would you like to focus on today?</p>
|
||||
</header>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
{/* Training Mode Card */}
|
||||
<Link href="/client/training" className="group">
|
||||
<motion.div
|
||||
whileHover={{ y: -5 }}
|
||||
className="bg-white p-10 rounded-[2rem] border border-zinc-200 shadow-xl group-hover:shadow-2xl group-hover:border-primary/30 transition-all h-full flex flex-col items-start"
|
||||
>
|
||||
<div className="w-16 h-16 rounded-2xl bg-blue-50 text-primary flex items-center justify-center mb-8 group-hover:scale-110 transition-transform">
|
||||
<PlayCircle className="w-8 h-8" />
|
||||
</div>
|
||||
<h2 className="text-3xl font-bold text-zinc-900 mb-2">Start Training</h2>
|
||||
<p className="text-zinc-500 mb-8 flex-1">Execute your assigned rehabilitation program. Follow real-time guidance and track your reps.</p>
|
||||
|
||||
<div className="flex items-center gap-2 text-primary font-bold uppercase tracking-widest text-sm group-hover:gap-4 transition-all">
|
||||
Begin Session <ArrowRight className="w-4 h-4" />
|
||||
</div>
|
||||
</motion.div>
|
||||
</Link>
|
||||
|
||||
{/* Live Monitor Card */}
|
||||
<Link href="/client/monitor" className="group">
|
||||
<motion.div
|
||||
whileHover={{ y: -5 }} // Corrected: removed "cursor-pointer" as Link handles it
|
||||
className="bg-zinc-900 p-10 rounded-[2rem] border border-zinc-800 shadow-xl group-hover:shadow-2xl group-hover:border-zinc-700 transition-all h-full flex flex-col items-start"
|
||||
>
|
||||
<div className="w-16 h-16 rounded-2xl bg-zinc-800 text-green-400 flex items-center justify-center mb-8 group-hover:scale-110 transition-transform">
|
||||
<Eye className="w-8 h-8" />
|
||||
</div>
|
||||
<h2 className="text-3xl font-bold text-white mb-2">Live Monitor</h2>
|
||||
<p className="text-zinc-400 mb-8 flex-1">Continuous activity recognition. Monitors posture (Sitting/Standing) and detects falls in real-time.</p>
|
||||
|
||||
<div className="flex items-center gap-2 text-green-400 font-bold uppercase tracking-widest text-sm group-hover:gap-4 transition-all">
|
||||
Launch Monitor <ArrowRight className="w-4 h-4" />
|
||||
</div>
|
||||
</motion.div>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Recent Activity Section */}
|
||||
<div className="mt-16 bg-white p-8 rounded-[2rem] border border-zinc-200 shadow-sm">
|
||||
<h3 className="text-xl font-bold text-zinc-800 mb-6 flex items-center gap-2">
|
||||
<ActivityIcon className="w-5 h-5 text-blue-500" />
|
||||
Recent Live Activity
|
||||
</h3>
|
||||
<ActivityList />
|
||||
</div>
|
||||
|
||||
<footer className="mt-16 text-center">
|
||||
<Link href="/" className="text-zinc-400 hover:text-zinc-600 text-sm transition-colors">Log Out</Link>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ActivityList() {
|
||||
const { user } = useAuth();
|
||||
const [logs, setLogs] = React.useState<any[]>([]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (user) {
|
||||
fetch('/api/logs', { headers: { 'x-user-id': user.id } })
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.logs) setLogs(data.logs);
|
||||
});
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
if (logs.length === 0) {
|
||||
return <div className="text-zinc-400 italic">No recent activity detected.</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left">
|
||||
<thead>
|
||||
<tr className="text-zinc-400 text-xs uppercase tracking-wider border-b border-zinc-100">
|
||||
<th className="pb-3 font-normal">Time</th>
|
||||
<th className="pb-3 font-normal">Status</th>
|
||||
<th className="pb-3 font-normal">Details</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-zinc-50">
|
||||
{logs.map((log) => (
|
||||
<tr key={log.id} className="group hover:bg-zinc-50 transition-colors">
|
||||
<td className="py-3 text-sm text-zinc-500">
|
||||
{new Date(log.timestamp).toLocaleTimeString()}
|
||||
</td>
|
||||
<td className="py-3">
|
||||
<span className={`inline-flex items-center px-2 py-1 rounded-md text-xs font-medium ${
|
||||
log.status.includes('Fall') || log.status.includes('ALARM')
|
||||
? 'bg-red-50 text-red-600 border border-red-100'
|
||||
: 'bg-green-50 text-green-600 border border-green-100'
|
||||
}`}>
|
||||
{log.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-3 text-sm text-zinc-400">
|
||||
{JSON.stringify(log.details)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
907
app/client/training/page.tsx
Normal file
|
|
@ -0,0 +1,907 @@
|
|||
'use client';
|
||||
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { HARCore } from '@/lib/pose/HARCore';
|
||||
// Import from the official tasks-vision package
|
||||
import { PoseLandmarker, FilesetResolver, DrawingUtils, PoseLandmarkerResult } from '@mediapipe/tasks-vision';
|
||||
import { RefreshCcw, ArrowLeft, PlayCircle, ChevronDown, ChevronUp } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { Suspense } from 'react';
|
||||
|
||||
import { AuthProvider, useAuth } from '@/lib/auth';
|
||||
|
||||
export default function TrainingPageWrap() {
|
||||
return (
|
||||
<AuthProvider>
|
||||
<Suspense fallback={<div className="min-h-screen bg-zinc-900 flex items-center justify-center text-white">Loading...</div>}>
|
||||
<TrainingPage />
|
||||
</Suspense>
|
||||
</AuthProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function TrainingPage() {
|
||||
const { user } = useAuth();
|
||||
const searchParams = useSearchParams();
|
||||
const mode = searchParams.get('mode');
|
||||
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isStarted, setIsStarted] = useState(false);
|
||||
|
||||
// Workflow State
|
||||
const [menu, setMenu] = useState<any>(null);
|
||||
const [currentExerciseIndex, setCurrentExerciseIndex] = useState(0);
|
||||
// const [currentSet, setCurrentSet] = useState(1); // REMOVED: Linear Progression uses index only
|
||||
const [repsOffset, setRepsOffset] = useState(0); // Offset for accumulated reps
|
||||
const [stats, setStats] = useState({ exercise: '', reps: 0, status: 'Idle', feedback: '', mae: 0});
|
||||
const [isWorkoutComplete, setIsWorkoutComplete] = useState(false);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
const [feedbackMsg, setFeedbackMsg] = useState<string>("");
|
||||
const [isWarning, setIsWarning] = useState<boolean>(false);
|
||||
|
||||
// UI State
|
||||
const [expandedSet, setExpandedSet] = useState<number | null>(null);
|
||||
|
||||
// Recap State
|
||||
const [results, setResults] = useState<any[]>([]);
|
||||
const maeBuffer = useRef<number[]>([]);
|
||||
|
||||
// Per-Rep Tracking
|
||||
const repBuffer = useRef<number[]>([]);
|
||||
const repFeedbackBuffer = useRef<string[]>([]); // Buffer for feedback text
|
||||
const lastRepCount = useRef(0);
|
||||
const currentSetReps = useRef<{rep: number, score: number, feedback: string}[]>([]);
|
||||
|
||||
// Rest Timer State
|
||||
const [isResting, setIsResting] = useState(false);
|
||||
const [restTimer, setRestTimer] = useState(0);
|
||||
|
||||
// Refs for loop
|
||||
const harRef = useRef<HARCore | null>(null);
|
||||
const landmarkerRef = useRef<PoseLandmarker | null>(null);
|
||||
const requestRef = useRef<number | null>(null);
|
||||
const isRestingRef = useRef(false);
|
||||
const isStartedRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
isRestingRef.current = isResting;
|
||||
}, [isResting]);
|
||||
|
||||
useEffect(() => {
|
||||
isStartedRef.current = isStarted;
|
||||
}, [isStarted]);
|
||||
|
||||
|
||||
// API Base URL
|
||||
const API_BASE = process.env.NEXT_PUBLIC_API_URL || '';
|
||||
|
||||
// Fetch Latest Menu
|
||||
const fetchMenu = async () => {
|
||||
// Check for Free Mode
|
||||
if (mode === 'free') {
|
||||
const local = localStorage.getItem('straps_free_mode_menu');
|
||||
if (local) {
|
||||
const menuData = JSON.parse(local);
|
||||
setMenu(menuData);
|
||||
setCurrentExerciseIndex(0);
|
||||
setRepsOffset(0);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!user) return;
|
||||
const headers = { 'x-user-id': user.id.toString() };
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/menus`, { headers });
|
||||
const data = await res.json();
|
||||
if (data && data.length > 0) {
|
||||
const latest = data[0];
|
||||
if (typeof latest.exercises === 'string') latest.exercises = JSON.parse(latest.exercises);
|
||||
setMenu(latest);
|
||||
setCurrentExerciseIndex(0);
|
||||
setRepsOffset(0);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch menu:", err);
|
||||
}
|
||||
};
|
||||
|
||||
// Init Logic and Load Models
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
|
||||
async function init() {
|
||||
try {
|
||||
// 1. Fetch Menu
|
||||
await fetchMenu();
|
||||
|
||||
// 2. Init Core
|
||||
const core = new HARCore();
|
||||
harRef.current = core;
|
||||
|
||||
// 3. Init Vision
|
||||
const vision = await FilesetResolver.forVisionTasks(
|
||||
"https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.14/wasm"
|
||||
);
|
||||
if (!isMounted) return;
|
||||
|
||||
const landmarker = await PoseLandmarker.createFromOptions(vision, {
|
||||
baseOptions: {
|
||||
modelAssetPath: `https://storage.googleapis.com/mediapipe-models/pose_landmarker/pose_landmarker_full/float16/1/pose_landmarker_full.task`,
|
||||
delegate: "GPU"
|
||||
},
|
||||
runningMode: "VIDEO",
|
||||
numPoses: 1
|
||||
});
|
||||
landmarkerRef.current = landmarker;
|
||||
|
||||
// 4. Init Camera
|
||||
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({
|
||||
video: { width: 640, height: 480 }
|
||||
});
|
||||
|
||||
if (videoRef.current) {
|
||||
videoRef.current.srcObject = stream;
|
||||
try {
|
||||
await videoRef.current.play();
|
||||
} catch (err) {
|
||||
console.warn("Video play aborted (harmless):", err);
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
requestRef.current = requestAnimationFrame(predictWebcam);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.error("Init Error:", e);
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
init();
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
if (requestRef.current) cancelAnimationFrame(requestRef.current);
|
||||
if (videoRef.current && videoRef.current.srcObject) {
|
||||
(videoRef.current.srcObject as MediaStream).getTracks().forEach(t => t.stop());
|
||||
}
|
||||
};
|
||||
}, [user, mode]); // Trigger init/fetch when mode changes
|
||||
|
||||
// Rest Timer Countdown
|
||||
useEffect(() => {
|
||||
let interval: NodeJS.Timeout;
|
||||
if (isResting && restTimer > 0) {
|
||||
interval = setInterval(() => {
|
||||
setRestTimer((prev) => prev - 1);
|
||||
}, 1000);
|
||||
} else if (isResting && restTimer <= 0) {
|
||||
// Rest Finished
|
||||
setIsResting(false);
|
||||
// Re-sync offset to ignore any movements during rest
|
||||
setRepsOffset(stats.reps);
|
||||
}
|
||||
return () => clearInterval(interval);
|
||||
}, [isResting, restTimer, stats.reps]);
|
||||
|
||||
// Effect: Update Active Exercise in HAR Core
|
||||
useEffect(() => {
|
||||
if (menu && harRef.current) {
|
||||
const range = menu.exercises?.[currentExerciseIndex];
|
||||
if (range) {
|
||||
harRef.current.setExercise(range.name);
|
||||
}
|
||||
}
|
||||
}, [menu, currentExerciseIndex]);
|
||||
|
||||
// Frame Loop Logic
|
||||
const lastVideoTimeRef = useRef(-1);
|
||||
|
||||
const predictWebcam = async () => {
|
||||
const video = videoRef.current;
|
||||
const canvas = canvasRef.current;
|
||||
const landmarker = landmarkerRef.current;
|
||||
const har = harRef.current;
|
||||
|
||||
if (video && canvas && landmarker && har) {
|
||||
let startTimeMs = performance.now();
|
||||
|
||||
if (lastVideoTimeRef.current !== video.currentTime && video.videoWidth > 0 && video.videoHeight > 0) {
|
||||
lastVideoTimeRef.current = video.currentTime;
|
||||
|
||||
const result = landmarker.detectForVideo(video, startTimeMs);
|
||||
|
||||
// Draw
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (ctx) {
|
||||
ctx.save();
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// Mirror
|
||||
ctx.scale(-1, 1);
|
||||
ctx.translate(-canvas.width, 0);
|
||||
|
||||
// Draw Video Frame
|
||||
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
||||
|
||||
if (result.landmarks) {
|
||||
const drawingUtils = new DrawingUtils(ctx);
|
||||
for (const lm of result.landmarks) {
|
||||
// Config
|
||||
const connectors = PoseLandmarker.POSE_CONNECTIONS;
|
||||
|
||||
// Draw Connections
|
||||
ctx.lineCap = 'round';
|
||||
ctx.lineJoin = 'round';
|
||||
|
||||
for (const { start, end } of connectors) {
|
||||
const p1 = lm[start];
|
||||
const p2 = lm[end];
|
||||
|
||||
if (!p1 || !p2 || (p1.visibility && p1.visibility < 0.5) || (p2.visibility && p2.visibility < 0.5)) continue;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(p1.x * canvas.width, p1.y * canvas.height);
|
||||
ctx.lineTo(p2.x * canvas.width, p2.y * canvas.height);
|
||||
|
||||
let color = '#00FFFF'; // Cyan
|
||||
let glow = '#00FFFF'; // Cyan Glow
|
||||
|
||||
ctx.shadowColor = glow;
|
||||
ctx.shadowBlur = 15;
|
||||
ctx.strokeStyle = color;
|
||||
ctx.lineWidth = 4;
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// Draw Joints
|
||||
for (let i = 0; i < lm.length; i++) {
|
||||
const p = lm[i];
|
||||
if (p.visibility && p.visibility < 0.5) continue;
|
||||
if (i < 11 && i !== 0) continue; // Keep nose (0)
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(p.x * canvas.width, p.y * canvas.height, 5, 0, 2 * Math.PI);
|
||||
ctx.fillStyle = '#FFFFFF'; // White core
|
||||
ctx.shadowColor = '#00FFFF'; // Cyan glow
|
||||
ctx.shadowBlur = 20;
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
ctx.shadowBlur = 0;
|
||||
}
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// Process Logic
|
||||
if (isStartedRef.current && !isRestingRef.current && result.landmarks && result.landmarks.length > 0) {
|
||||
// Pass normalized landmarks (x,y,z,visibility) AND world landmarks (meters)
|
||||
const res = await har.process(
|
||||
result.landmarks[0] as any,
|
||||
result.worldLandmarks[0] as any
|
||||
);
|
||||
|
||||
if (res) {
|
||||
// Accumulate Form Score (MAE)
|
||||
if (res.debug && (res.debug as any).scores && (res.debug as any).scores.deviation_mae) {
|
||||
const val = (res.debug as any).scores.deviation_mae;
|
||||
if (val > 0) {
|
||||
maeBuffer.current.push(val);
|
||||
repBuffer.current.push(val); // Push to current rep buffer
|
||||
}
|
||||
}
|
||||
|
||||
// Capture Feedback Text
|
||||
if (res.feedback && res.feedback.trim() !== "" && !res.feedback.includes("null")) {
|
||||
// Only push meaningful feedback
|
||||
repFeedbackBuffer.current.push(res.feedback);
|
||||
}
|
||||
|
||||
// --- Rep Completion Logic ---
|
||||
if (res.reps > lastRepCount.current) {
|
||||
// Rep Finished!
|
||||
const avgRepScore = repBuffer.current.length > 0
|
||||
? repBuffer.current.reduce((a, b) => a + b, 0) / repBuffer.current.length
|
||||
: 0;
|
||||
|
||||
// Calculate Dominant Feedback
|
||||
let dominantFeedback = "Perfect";
|
||||
if (repFeedbackBuffer.current.length > 0) {
|
||||
// Find most frequent string
|
||||
const counts: Record<string, number> = {};
|
||||
let maxCount = 0;
|
||||
let maxKey = "";
|
||||
|
||||
for (const fb of repFeedbackBuffer.current) {
|
||||
const cleanFb = fb.trim();
|
||||
counts[cleanFb] = (counts[cleanFb] || 0) + 1;
|
||||
if (counts[cleanFb] > maxCount) {
|
||||
maxCount = counts[cleanFb];
|
||||
maxKey = cleanFb;
|
||||
}
|
||||
}
|
||||
if (maxKey) dominantFeedback = maxKey;
|
||||
}
|
||||
|
||||
currentSetReps.current.push({
|
||||
rep: res.reps,
|
||||
score: avgRepScore,
|
||||
feedback: dominantFeedback
|
||||
});
|
||||
|
||||
// Reset for next rep
|
||||
repBuffer.current = [];
|
||||
repFeedbackBuffer.current = [];
|
||||
lastRepCount.current = res.reps;
|
||||
}
|
||||
|
||||
setStats({
|
||||
status: res.status,
|
||||
exercise: res.exercise || 'Unknown',
|
||||
reps: res.reps, // Reps from RehabCore
|
||||
feedback: res.feedback,
|
||||
mae: (res.debug as any)?.scores?.deviation_mae || 0
|
||||
});
|
||||
|
||||
// Update Feedback UI State
|
||||
if (res.feedback) {
|
||||
setFeedbackMsg(res.feedback);
|
||||
setIsWarning(res.feedback.includes("⚠️"));
|
||||
} else {
|
||||
setFeedbackMsg("");
|
||||
setIsWarning(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
requestRef.current = requestAnimationFrame(predictWebcam);
|
||||
};
|
||||
|
||||
// Progression Logic
|
||||
useEffect(() => {
|
||||
if (!menu || isWorkoutComplete) return;
|
||||
const currentTarget = menu.exercises[currentExerciseIndex];
|
||||
if (!currentTarget) {
|
||||
finishWorkout();
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate Reps in Current Set
|
||||
const currentRepsInSet = Math.max(0, stats.reps - repsOffset);
|
||||
|
||||
const isMatchingExercise = stats.exercise && numberSafeMatch(stats.exercise, currentTarget.name);
|
||||
|
||||
if (isMatchingExercise) {
|
||||
if (currentRepsInSet >= currentTarget.reps) {
|
||||
// --- SET COMPLETE LOGIC ---
|
||||
|
||||
// 1. Calculate Average Form Score
|
||||
const avgMae = maeBuffer.current.length > 0
|
||||
? maeBuffer.current.reduce((a, b) => a + b, 0) / maeBuffer.current.length
|
||||
: 0;
|
||||
|
||||
// 2. Save Result
|
||||
setResults(prev => [...prev, {
|
||||
name: currentTarget.name,
|
||||
set: currentTarget.set_index || 1,
|
||||
reps: currentRepsInSet,
|
||||
weight: currentTarget.weight,
|
||||
score: avgMae,
|
||||
repDetails: [...currentSetReps.current] // CAPTURE REP DETAILS
|
||||
}]);
|
||||
|
||||
// 3. Reset Buffers
|
||||
maeBuffer.current = [];
|
||||
repBuffer.current = [];
|
||||
repFeedbackBuffer.current = []; // Reset feedback too
|
||||
currentSetReps.current = [];
|
||||
lastRepCount.current = 0; // Reset for next set
|
||||
|
||||
// Linear Logic: Next Exercise in List
|
||||
const nextExIdx = currentExerciseIndex + 1;
|
||||
const restTime = (currentTarget as any).rest_time_seconds || 0;
|
||||
|
||||
if (nextExIdx >= menu.exercises.length) {
|
||||
finishWorkout();
|
||||
} else {
|
||||
setCurrentExerciseIndex(nextExIdx);
|
||||
setRepsOffset(stats.reps); // Important: Offset total reps
|
||||
|
||||
if (restTime > 0) {
|
||||
setIsResting(true);
|
||||
setRestTimer(restTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [stats.reps, stats.exercise, menu, currentExerciseIndex, repsOffset]);
|
||||
|
||||
const numberSafeMatch = (a: string, b: string) => {
|
||||
return a.toLowerCase().includes(b.split(' ')[0].toLowerCase());
|
||||
}
|
||||
|
||||
const saveRecap = async (summary: any) => {
|
||||
if (!menu || !user) return;
|
||||
try {
|
||||
await fetch(`${API_BASE}/api/recap`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-user-id': user.id
|
||||
},
|
||||
body: JSON.stringify({
|
||||
menu_id: menu.id,
|
||||
user_id: user.id,
|
||||
summary: {
|
||||
...summary,
|
||||
detailed_results: results // Send detailed results to backend
|
||||
}
|
||||
})
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Failed to save recap:", e);
|
||||
}
|
||||
};
|
||||
|
||||
const finishWorkout = async () => {
|
||||
if (isWorkoutComplete || !menu) return;
|
||||
setIsWorkoutComplete(true);
|
||||
setIsSaving(true);
|
||||
try {
|
||||
await saveRecap({
|
||||
completed: true,
|
||||
exercises: menu.exercises,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
} catch (e) { console.error(e); }
|
||||
finally { setIsSaving(false); }
|
||||
};
|
||||
|
||||
// Helper for Form Grade
|
||||
const getGrade = (mae: number) => {
|
||||
if (mae < 8) return { letter: 'S', color: 'text-purple-400', label: 'Excellent' };
|
||||
if (mae < 15) return { letter: 'A', color: 'text-green-400', label: 'Good' };
|
||||
if (mae < 25) return { letter: 'B', color: 'text-yellow-400', label: 'Fair' };
|
||||
return { letter: 'C', color: 'text-red-400', label: 'Needs Improvement' };
|
||||
};
|
||||
|
||||
if (isWorkoutComplete) {
|
||||
return (
|
||||
<div className="min-h-screen bg-zinc-950 text-white flex flex-col items-center justify-center p-4 font-sans">
|
||||
<div className="max-w-2xl w-full bg-zinc-900 rounded-3xl border border-zinc-800 p-8 shadow-2xl relative overflow-hidden">
|
||||
{/* Cyberpunk Glow */}
|
||||
<div className="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-blue-500 via-primary to-purple-500"></div>
|
||||
|
||||
<div className="text-center mb-8">
|
||||
<div className="inline-block px-4 py-1 rounded-full bg-green-500/10 text-green-400 text-xs font-bold tracking-widest uppercase mb-4 border border-green-500/20">
|
||||
Session Complete
|
||||
</div>
|
||||
<h1 className="text-4xl font-black text-white tracking-tight mb-2">TRAINING RECAP</h1>
|
||||
<p className="text-zinc-500 text-sm">Excellent work. Here is your performance breakdown.</p>
|
||||
</div>
|
||||
|
||||
{/* Stats Grid */}
|
||||
<div className="grid grid-cols-2 gap-4 mb-8">
|
||||
<div className="bg-zinc-950/50 p-6 rounded-2xl border border-zinc-800 text-center">
|
||||
<div className="text-3xl font-black text-white">{results.length}</div>
|
||||
<div className="text-xs font-bold text-zinc-500 uppercase tracking-widest mt-1">Sets Completed</div>
|
||||
</div>
|
||||
<div className="bg-zinc-950/50 p-6 rounded-2xl border border-zinc-800 text-center">
|
||||
<div className="text-3xl font-black text-primary">
|
||||
{results.reduce((a, b) => a + b.reps, 0)}
|
||||
</div>
|
||||
<div className="text-xs font-bold text-zinc-500 uppercase tracking-widest mt-1">Total Reps</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Detailed Results Table */}
|
||||
<div className="bg-zinc-950/30 rounded-2xl border border-zinc-800 overflow-hidden mb-8 max-h-[40vh] overflow-y-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-zinc-900 border-b border-zinc-800">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left font-bold text-zinc-500 uppercase tracking-wider text-[10px]">Exercise</th>
|
||||
<th className="px-4 py-3 text-center font-bold text-zinc-500 uppercase tracking-wider text-[10px]">Set</th>
|
||||
<th className="px-4 py-3 text-center font-bold text-zinc-500 uppercase tracking-wider text-[10px]">Load</th>
|
||||
<th className="px-4 py-3 text-right font-bold text-zinc-500 uppercase tracking-wider text-[10px]">Form Score</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-zinc-800">
|
||||
{results.map((res, i) => {
|
||||
const grade = getGrade(res.score);
|
||||
const isExpanded = expandedSet === i;
|
||||
|
||||
return (
|
||||
<React.Fragment key={i}>
|
||||
<tr
|
||||
onClick={() => setExpandedSet(isExpanded ? null : i)}
|
||||
className="hover:bg-zinc-800/50 transition-colors cursor-pointer group"
|
||||
>
|
||||
<td className="px-4 py-3 font-medium text-white flex items-center gap-2">
|
||||
{isExpanded ? <ChevronUp size={14} className="text-zinc-500" /> : <ChevronDown size={14} className="text-zinc-500" />}
|
||||
{res.name}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-center text-zinc-400 font-mono">#{res.set}</td>
|
||||
<td className="px-4 py-3 text-center text-zinc-400">
|
||||
{res.reps}x <span className="text-zinc-600">@</span> {res.weight}kg
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right">
|
||||
<div className="flex flex-col items-end gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`font-black ${grade.color}`}>{grade.label}</span>
|
||||
<span className="text-[10px] text-zinc-600 font-mono">Avg: {res.score.toFixed(1)}°</span>
|
||||
</div>
|
||||
{/* Preview Chips */}
|
||||
{!isExpanded && res.repDetails && res.repDetails.length > 0 && (
|
||||
<div className="flex justify-end gap-1">
|
||||
{res.repDetails.map((r: any, idx: number) => (
|
||||
<div key={idx} className={`w-1.5 h-1.5 rounded-full ${getGrade(r.score).color.replace('text-','bg-')}`} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{/* Expanded Detail Row */}
|
||||
{isExpanded && (
|
||||
<tr className="bg-zinc-900/50">
|
||||
<td colSpan={4} className="px-4 py-4">
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2">
|
||||
{res.repDetails?.map((r: any, idx: number) => {
|
||||
const rGrade = getGrade(r.score);
|
||||
const isPerfect = rGrade.label === 'Excellent';
|
||||
const hasFeedback = r.feedback && r.feedback !== 'Perfect';
|
||||
|
||||
return (
|
||||
<div key={idx} className="bg-zinc-950 border border-zinc-800 rounded-lg p-3 flex flex-col gap-1">
|
||||
<div className="flex justify-between items-center w-full">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs font-mono text-zinc-500">#{idx + 1}</span>
|
||||
<span className={`text-xs font-bold ${rGrade.color}`}>{rGrade.label}</span>
|
||||
</div>
|
||||
<span className="text-[10px] text-zinc-600 font-mono">{r.score.toFixed(1)}°</span>
|
||||
</div>
|
||||
|
||||
{/* Feedback Text */}
|
||||
<div className={`text-[10px] uppercase font-bold tracking-wide ${hasFeedback ? 'text-zinc-400' : 'text-zinc-600/50'}`}>
|
||||
{hasFeedback ? `"${r.feedback}"` : "NO ISSUES DETECTED"}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
{(!res.repDetails || res.repDetails.length === 0) && (
|
||||
<div className="col-span-3 text-center text-zinc-500 text-xs italic py-2">No individual rep data available.</div>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4">
|
||||
<Link href="/client" className="flex-1 px-6 py-4 bg-white text-black font-bold uppercase tracking-widest rounded-xl hover:bg-zinc-200 transition-colors text-center text-sm">
|
||||
Back to Dashboard
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ... Render same as before ...
|
||||
const currentTarget = menu?.exercises?.[currentExerciseIndex];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background text-foreground p-6 font-sans selection:bg-primary/30">
|
||||
<header className="flex justify-between items-center mb-8">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link href="/client" className="p-2 bg-white rounded-full hover:bg-zinc-100 transition-colors border border-zinc-200">
|
||||
<ArrowLeft className="w-5 h-5 text-zinc-600" />
|
||||
</Link>
|
||||
<h1 className="text-3xl font-light tracking-widest text-zinc-800">TRAINING<span className="font-bold text-primary">.MODE</span></h1>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="bg-white border border-zinc-200 px-6 py-2 rounded-full text-xs font-medium tracking-wider text-zinc-600 uppercase shadow-sm">
|
||||
{menu ? menu.name : 'Loading...'}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
setMenu(null);
|
||||
fetchMenu();
|
||||
}}
|
||||
className="p-2 bg-white border border-zinc-200 hover:bg-zinc-100 rounded-full transition-colors shadow-sm"
|
||||
title="Refresh Menu"
|
||||
>
|
||||
<RefreshCcw size={18} className="text-zinc-500" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
// Reset Logic
|
||||
// setCurrentSet(1);
|
||||
setRepsOffset(0);
|
||||
setStats(prev => ({ ...prev, reps: 0, status: 'Idle', feedback: 'Reset' }));
|
||||
if (harRef.current) harRef.current.resetParams();
|
||||
setIsResting(false);
|
||||
}}
|
||||
className="px-4 py-2 bg-red-50 text-red-600 rounded-full text-xs font-bold uppercase tracking-widest border border-red-100 hover:bg-red-100 transition-colors"
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
|
||||
<div className="flex bg-zinc-100 p-1 rounded-full border border-zinc-200">
|
||||
<Link
|
||||
href="/client/training"
|
||||
className={`px-4 py-1.5 rounded-full text-xs font-bold uppercase tracking-widest transition-all ${
|
||||
!(new URLSearchParams(typeof window !== 'undefined' ? window.location.search : '').get('mode') === 'free')
|
||||
? 'bg-white text-primary shadow-sm'
|
||||
: 'text-zinc-400 hover:text-zinc-600'
|
||||
}`}
|
||||
>
|
||||
Assigned
|
||||
</Link>
|
||||
<Link
|
||||
href="/client/training?mode=free"
|
||||
className={`px-4 py-1.5 rounded-full text-xs font-bold uppercase tracking-widest transition-all ${
|
||||
(new URLSearchParams(typeof window !== 'undefined' ? window.location.search : '').get('mode') === 'free')
|
||||
? 'bg-white text-primary shadow-sm'
|
||||
: 'text-zinc-400 hover:text-zinc-600'
|
||||
}`}
|
||||
>
|
||||
Personal
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<div className="lg:col-span-2 relative border-8 border-white rounded-[2rem] overflow-hidden bg-zinc-100 shadow-2xl">
|
||||
{isLoading && <div className="absolute inset-0 flex items-center justify-center text-blue-400 font-mono animate-pulse">Loading AI Engine (WASM)...</div>}
|
||||
<video ref={videoRef} className="hidden" width="640" height="480" autoPlay playsInline muted />
|
||||
<canvas ref={canvasRef} width="640" height="480" className="w-full h-auto object-contain" />
|
||||
|
||||
{/* Status Display Removed as per user request (kept in Live Monitor) */}
|
||||
{/* <div className="absolute top-6 left-6 flex gap-4">
|
||||
<div className={`px-4 py-1.5 rounded-full text-xs font-bold tracking-widest backdrop-blur-md border ${stats.status === 'Fall Detected' ? 'bg-red-500/20 border-red-500 text-red-400' : 'bg-primary/10 border-primary/30 text-primary'}`}>
|
||||
{stats.status.toUpperCase()}
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
{/* --- NEW: FEEDBACK OVERLAY WINDOW --- */}
|
||||
|
||||
|
||||
{/* Rest Overlay */}
|
||||
{isResting && (
|
||||
<div className="absolute inset-0 bg-black/95 z-50 flex flex-col items-center justify-center animate-in fade-in duration-500">
|
||||
<div className="text-secondary font-medium tracking-[0.2em] uppercase mb-6 text-sm">Recovery Break</div>
|
||||
<div className="text-9xl font-light text-highlight mb-10 tabular-nums tracking-tighter">
|
||||
{Math.floor(restTimer / 60)}:{(restTimer % 60).toString().padStart(2, '0')}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setIsResting(false)}
|
||||
className="px-10 py-3 bg-secondary/10 hover:bg-secondary/20 hover:scale-105 text-secondary rounded-full font-medium transition-all text-xs uppercase tracking-widest border border-secondary/30"
|
||||
>
|
||||
Resume Workout
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Start Overlay */}
|
||||
{!isStarted && !isLoading && (
|
||||
<div className="absolute inset-0 bg-white/80 backdrop-blur-sm z-40 flex flex-col items-center justify-center p-8 text-center">
|
||||
<h2 className="text-3xl font-bold text-zinc-900 mb-2">Ready to Train?</h2>
|
||||
<p className="text-zinc-600 mb-8 max-w-md">
|
||||
{menu ? `Start your assigned program: ${menu.name}` : `No assigned program found.`}
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
{menu && (
|
||||
<button
|
||||
onClick={() => setIsStarted(true)}
|
||||
className="px-12 py-4 bg-primary text-white text-lg font-bold rounded-full shadow-xl hover:shadow-2xl hover:scale-105 transition-all flex items-center justify-center gap-3 w-64"
|
||||
>
|
||||
<PlayCircle className="w-6 h-6" /> START {menu.name ? 'PROGRAM' : 'WORKOUT'}
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-4 w-64">
|
||||
<div className="h-px bg-zinc-300 flex-1"></div>
|
||||
<span className="text-xs text-zinc-400 font-bold uppercase">OR</span>
|
||||
<div className="h-px bg-zinc-300 flex-1"></div>
|
||||
</div>
|
||||
|
||||
<Link
|
||||
href="/client/free"
|
||||
className="px-12 py-4 bg-white border-2 border-zinc-200 text-zinc-600 text-lg font-bold rounded-full hover:border-primary hover:text-primary transition-all flex items-center justify-center gap-3 w-64 text-center"
|
||||
>
|
||||
CREATE PERSONAL MENU
|
||||
</Link>
|
||||
|
||||
{new URLSearchParams(typeof window !== 'undefined' ? window.location.search : '').get('mode') === 'free' && (
|
||||
<Link
|
||||
href="/client/training"
|
||||
className="mt-2 text-zinc-400 text-xs font-bold uppercase tracking-widest hover:text-primary transition-colors"
|
||||
>
|
||||
Return to Assigned Program
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-6">
|
||||
{/* Workout Menu List */}
|
||||
<div className="bg-white rounded-2xl border border-zinc-200 overflow-hidden flex flex-col max-h-[40vh] shadow-lg">
|
||||
<div className="p-4 border-b border-zinc-100 bg-zinc-50">
|
||||
<h2 className="text-zinc-500 text-xs font-bold uppercase tracking-widest">Workout Plan</h2>
|
||||
</div>
|
||||
<div className="overflow-y-auto p-2 space-y-2">
|
||||
{menu?.exercises?.map((ex: any, idx: number) => {
|
||||
const isActive = idx === currentExerciseIndex;
|
||||
const isCompleted = idx < currentExerciseIndex;
|
||||
|
||||
return (
|
||||
<React.Fragment key={idx}>
|
||||
{(idx === 0 || ex.set_index > (menu.exercises[idx - 1]?.set_index || 0)) && (
|
||||
<div className="py-4 flex items-center gap-4">
|
||||
<div className="h-px bg-zinc-200 flex-1"></div>
|
||||
<span className="text-xs font-black text-zinc-400 uppercase tracking-[0.2em] bg-white px-2 rounded-lg">
|
||||
SET {ex.set_index || 1}
|
||||
</span>
|
||||
<div className="h-px bg-zinc-200 flex-1"></div>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={`p-5 rounded-2xl transition-all border ${
|
||||
isActive
|
||||
? 'bg-blue-50 border-primary shadow-sm'
|
||||
: isCompleted
|
||||
? 'opacity-40 grayscale border-transparent'
|
||||
: 'bg-white border-zinc-100'
|
||||
}`}
|
||||
>
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<div className={`text-lg tracking-wide ${isActive ? 'font-bold text-zinc-900' : 'text-zinc-500 font-medium'}`}>
|
||||
{ex.name}
|
||||
</div>
|
||||
<div className="text-[10px] text-secondary/70 uppercase tracking-widest mt-1">
|
||||
Target: {ex.reps} reps • {ex.weight}kg
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isActive && (
|
||||
<div className="text-right">
|
||||
<div className="text-3xl font-light text-primary">
|
||||
{Math.max(0, stats.reps - repsOffset)}<span className="text-sm text-secondary/50 font-normal ml-1">/ {ex.reps}</span>
|
||||
</div>
|
||||
<div className="text-[10px] text-blue-300 uppercase tracking-wider font-bold animate-pulse">
|
||||
Set {ex.set_index || 1}/{ex.total_sets || 1}
|
||||
</div>
|
||||
{(ex as any).rest_time_seconds > 0 && (
|
||||
<div className="text-[10px] text-zinc-500 mt-1">
|
||||
Rest: {(ex as any).rest_time_seconds}s
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isCompleted && (
|
||||
<div className="text-green-500">
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
|
||||
{!menu && (
|
||||
<div className="p-4 text-center text-zinc-500 italic">No menu loaded</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-4 rounded-2xl border border-zinc-200 flex flex-col justify-center items-center text-center shadow-lg h-48">
|
||||
<h2 className="text-zinc-400 text-[10px] font-bold uppercase tracking-widest mb-2">Real-time Counter</h2>
|
||||
|
||||
<div className="relative">
|
||||
<svg className="w-32 h-32 transform -rotate-90">
|
||||
<circle cx="64" cy="64" r="56" stroke="currentColor" strokeWidth="6" fill="transparent" className="text-zinc-100" />
|
||||
<circle
|
||||
cx="64" cy="64" r="56"
|
||||
stroke="currentColor" strokeWidth="6" fill="transparent"
|
||||
className="text-primary transition-all duration-500 ease-out drop-shadow-md"
|
||||
strokeDasharray={2 * Math.PI * 56}
|
||||
strokeDashoffset={2 * Math.PI * 56 * (1 - (Math.max(0, stats.reps - repsOffset) / (currentTarget?.reps || 1)))}
|
||||
/>
|
||||
</svg>
|
||||
<div className="absolute inset-0 flex items-center justify-center flex-col">
|
||||
<span className="text-4xl font-black text-zinc-900">{Math.max(0, stats.reps - repsOffset)}</span>
|
||||
<span className="text-zinc-400 text-[10px] font-medium">REPS</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Redesigned Cyberpunk Feedback Card (Expanded) */}
|
||||
<div className={`p-0.5 rounded-xl flex-1 bg-gradient-to-r ${
|
||||
isWarning ? 'from-red-500 via-rose-500 to-red-500 animate-pulse' : 'from-cyan-400 via-blue-500 to-cyan-400'
|
||||
}`}>
|
||||
<div className="bg-zinc-50 rounded-[10px] p-6 h-full relative overflow-hidden flex flex-col justify-center">
|
||||
{/* Scanline effect (Subtle Light Mode) */}
|
||||
<div className="absolute inset-0 bg-[linear-gradient(rgba(0,0,0,0)_50%,rgba(0,0,0,0.02)_50%),linear-gradient(90deg,rgba(0,0,0,0.03),rgba(0,0,0,0.01),rgba(0,0,0,0.03))] z-0 pointer-events-none bg-[length:100%_4px,6px_100%]"></div>
|
||||
|
||||
<div className="relative z-10 text-center md:text-left">
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<h3 className={`text-xs font-black uppercase tracking-[0.2em] ${
|
||||
isWarning ? 'text-red-500 drop-shadow-sm' : 'text-cyan-600 drop-shadow-sm'
|
||||
}`}>
|
||||
{isWarning ? 'CRITICAL ERROR' : 'SYSTEM ADVICE'}
|
||||
</h3>
|
||||
{isWarning && <div className="w-3 h-3 bg-red-500 rounded-full animate-ping"></div>}
|
||||
</div>
|
||||
<p className={`text-2xl font-bold leading-tight uppercase font-mono break-words ${
|
||||
isWarning ? 'text-red-600' : 'text-zinc-800'
|
||||
}`}>
|
||||
{(stats.feedback || "SYSTEM READY").replace(/⚠️|✅|❌/g, '').replace(" | ", "\n").trim() || "WAITING FOR INPUT..."}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Form Quality (MAE) - Expanded */}
|
||||
<div className={`rounded-xl border-l-8 overflow-hidden shadow-lg bg-white h-32 flex flex-col justify-center ${
|
||||
stats.mae > 15
|
||||
? 'border-red-500'
|
||||
: stats.mae > 5
|
||||
? 'border-yellow-400'
|
||||
: 'border-emerald-500'
|
||||
}`}>
|
||||
<div className="px-6 py-2 flex justify-between items-center h-full">
|
||||
<div className="flex flex-col text-left">
|
||||
<span className="text-xs font-black uppercase tracking-widest text-zinc-400">Form Quality</span>
|
||||
<span className={`text-2xl uppercase font-black mt-1 ${
|
||||
stats.mae > 15 ? 'text-red-600' : stats.mae > 5 ? 'text-yellow-600' : 'text-emerald-600'
|
||||
}`}>
|
||||
{stats.mae > 15 ? 'Needs Improvement' : stats.mae > 5 ? 'Fair' : 'Excellent'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-5xl font-black tabular-nums leading-none text-zinc-900 tracking-tighter">
|
||||
{stats.mae.toFixed(1)}°
|
||||
</div>
|
||||
<span className="text-[10px] uppercase text-zinc-400 tracking-wider font-bold">Deviation</span>
|
||||
</div>
|
||||
</div>
|
||||
{/* Mini Graph Bar */}
|
||||
<div className="h-2 w-full bg-zinc-100 flex mt-auto">
|
||||
<div
|
||||
className={`h-full transition-all duration-500 ${stats.mae > 15 ? 'bg-red-500' : stats.mae > 5 ? 'bg-yellow-400' : 'bg-emerald-500'}`}
|
||||
style={{ width: `${Math.min(100, (stats.mae / 30) * 100)}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
380
app/coach/dashboard/page.tsx
Normal file
|
|
@ -0,0 +1,380 @@
|
|||
|
||||
'use client';
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Activity, User, Clock, ShieldAlert, Plus, Users, UserPlus } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
|
||||
import { AuthProvider, useAuth } from '@/lib/auth';
|
||||
|
||||
export default function DashboardPageWrap() {
|
||||
return (
|
||||
<AuthProvider>
|
||||
<DashboardPage />
|
||||
</AuthProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function DashboardPage() {
|
||||
const { user } = useAuth();
|
||||
const [greeting, setGreeting] = useState('');
|
||||
const [stats, setStats] = useState({
|
||||
totalMenus: 0,
|
||||
totalSessions: 0, // All time
|
||||
sessionsToday: 0,
|
||||
|
||||
recentMenus: [] as any[],
|
||||
recentRecaps: [] as any[],
|
||||
linkedClients: [] as any[]
|
||||
});
|
||||
|
||||
// Add Client State
|
||||
const [isAddingClient, setIsAddingClient] = useState(false);
|
||||
const [clientIdToAdd, setClientIdToAdd] = useState('');
|
||||
const [addClientStatus, setAddClientStatus] = useState('');
|
||||
|
||||
const loadData = React.useCallback(async () => {
|
||||
if (!user) return;
|
||||
|
||||
// Security Check
|
||||
if (user.role !== 'COACH') {
|
||||
window.location.href = '/';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const headers = { 'x-user-id': user.id };
|
||||
|
||||
// Fetch Recaps
|
||||
const resRecaps = await fetch('/api/recap', { headers });
|
||||
const recaps = await resRecaps.json();
|
||||
|
||||
// Fetch Menus
|
||||
const resMenus = await fetch('/api/menus', { headers });
|
||||
const menus = await resMenus.json();
|
||||
|
||||
// Fetch Linked Clients
|
||||
const resClients = await fetch(`/api/users?coachId=${user.id}`);
|
||||
const clients = await resClients.json();
|
||||
|
||||
if (Array.isArray(recaps) && Array.isArray(menus)) {
|
||||
const today = new Date().toDateString();
|
||||
const todaysRecaps = recaps.filter((r: any) => new Date(r.completed_at).toDateString() === today);
|
||||
|
||||
setStats({
|
||||
totalMenus: menus.length,
|
||||
totalSessions: recaps.length,
|
||||
sessionsToday: todaysRecaps.length,
|
||||
recentMenus: menus.sort((a: any, b: any) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()).slice(0, 5),
|
||||
recentRecaps: recaps.sort((a: any, b: any) => new Date(b.completed_at).getTime() - new Date(a.completed_at).getTime()).slice(0, 5),
|
||||
linkedClients: Array.isArray(clients) ? clients : []
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to load dashboard stats", e);
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
useEffect(() => {
|
||||
const hour = new Date().getHours();
|
||||
if (hour < 12) setGreeting('Good Morning');
|
||||
else if (hour < 18) setGreeting('Good Afternoon');
|
||||
else setGreeting('Good Evening');
|
||||
|
||||
loadData();
|
||||
}, [user, loadData]);
|
||||
|
||||
const handleAddClient = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!user || !clientIdToAdd) return;
|
||||
setAddClientStatus('Linking...');
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/coach/link-client', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ coachId: user.id, clientId: clientIdToAdd })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
setAddClientStatus('Client Linked!');
|
||||
setClientIdToAdd('');
|
||||
setIsAddingClient(false);
|
||||
// Refresh data
|
||||
await loadData();
|
||||
} else {
|
||||
setAddClientStatus(data.error || 'Failed to link');
|
||||
}
|
||||
} catch (e) {
|
||||
setAddClientStatus('Error linking client');
|
||||
}
|
||||
};
|
||||
|
||||
// Staggered animation variants
|
||||
const container = {
|
||||
hidden: { opacity: 0 },
|
||||
show: {
|
||||
opacity: 1,
|
||||
transition: { staggerChildren: 0.1 }
|
||||
}
|
||||
};
|
||||
|
||||
const item = {
|
||||
hidden: { opacity: 0, y: 20 },
|
||||
show: { opacity: 1, y: 0 }
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background text-foreground p-10 font-sans selection:bg-primary/30">
|
||||
<header className="mb-16 border-b border-zinc-200 pb-8 flex flex-col md:flex-row justify-between items-end gap-6">
|
||||
<div>
|
||||
<h1 className="text-5xl font-light tracking-tight text-zinc-800 mb-2">
|
||||
{greeting}, <span className="font-bold text-primary">{user?.name || 'Coach'}</span>.
|
||||
</h1>
|
||||
<p className="text-secondary text-sm tracking-wide mt-2 flex items-center gap-3">
|
||||
<span className="w-2 h-2 rounded-full bg-green-500 shadow-[0_0_8px_rgba(34,197,94,0.5)]"></span>
|
||||
SYSTEM ACTIVE
|
||||
</p>
|
||||
</div>
|
||||
<Link href="/coach/menu/new" className="bg-primary hover:bg-primary/90 text-black px-8 py-3 rounded-full font-bold transition-all hover:scale-105 shadow-[0_0_20px_-5px_var(--color-primary)] flex items-center gap-2 text-sm tracking-wide">
|
||||
<Plus className="w-4 h-4" /> NEW PROGRAM
|
||||
</Link>
|
||||
</header>
|
||||
|
||||
<motion.div
|
||||
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"
|
||||
variants={container}
|
||||
initial="hidden"
|
||||
animate="show"
|
||||
>
|
||||
<StatsCard
|
||||
title="Total Training Menus"
|
||||
value={stats.totalMenus.toString()}
|
||||
icon={<Activity className="text-blue-400" />}
|
||||
variant={item}
|
||||
/>
|
||||
<StatsCard
|
||||
title="Total Sessions (All Time)"
|
||||
value={stats.totalSessions.toString()}
|
||||
icon={<User className="text-purple-400" />}
|
||||
variant={item}
|
||||
/>
|
||||
<StatsCard
|
||||
title="Sessions Today"
|
||||
value={stats.sessionsToday.toString()}
|
||||
icon={<ShieldAlert className="text-green-400" />}
|
||||
variant={item}
|
||||
/>
|
||||
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
className="mt-12 bg-white border border-zinc-200 rounded-xl p-6 shadow-sm"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.5 }}
|
||||
>
|
||||
<h2 className="text-xl font-bold mb-4">Live Activity Feed</h2>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-4 text-sm text-gray-400">
|
||||
<span className="w-16">Now</span>
|
||||
<span className="text-zinc-900">System monitoring active...</span>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* My Clients Section */}
|
||||
<motion.div
|
||||
className="mt-6 bg-white border border-zinc-200 rounded-xl p-6 shadow-sm"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.55 }}
|
||||
>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="text-xl font-bold text-zinc-900">My Clients</h2>
|
||||
<button
|
||||
onClick={() => setIsAddingClient(!isAddingClient)}
|
||||
className="text-primary hover:text-blue-700 text-sm font-bold flex items-center gap-1 bg-blue-50 px-3 py-1 rounded-full transition-colors"
|
||||
>
|
||||
<UserPlus className="w-4 h-4" /> Add Client
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{isAddingClient && (
|
||||
<form onSubmit={handleAddClient} className="mb-6 bg-zinc-50 p-4 rounded-xl border border-zinc-200 animate-in fade-in slide-in-from-top-2 flex items-center gap-4">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Enter Client ID"
|
||||
value={clientIdToAdd}
|
||||
onChange={(e) => setClientIdToAdd(e.target.value)}
|
||||
className="bg-white border border-zinc-200 rounded-lg px-4 py-2 text-sm w-48 font-mono"
|
||||
/>
|
||||
<button type="submit" className="bg-zinc-900 text-white px-4 py-2 rounded-lg text-sm font-bold hover:bg-black">
|
||||
Link
|
||||
</button>
|
||||
{addClientStatus && <span className="text-xs font-bold text-primary">{addClientStatus}</span>}
|
||||
</form>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{stats.linkedClients.map((client: any) => (
|
||||
<div key={client.id} className="p-4 rounded-xl border border-zinc-100 bg-zinc-50 flex items-center gap-4">
|
||||
<div className="w-10 h-10 rounded-full bg-blue-100 flex items-center justify-center text-primary font-bold">
|
||||
{client.name.charAt(0)}
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-bold text-zinc-900">{client.name}</div>
|
||||
<div className="text-xs text-zinc-500">ID: {client.id}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{stats.linkedClients.length === 0 && (
|
||||
<div className="col-span-3 text-center py-8 text-zinc-400 italic">No clients linked yet. Add one above.</div>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Recent Menus List */}
|
||||
<motion.div
|
||||
className="mt-6 bg-white border border-zinc-200 rounded-xl p-6 shadow-sm"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.6 }}
|
||||
>
|
||||
<h2 className="text-xl font-bold mb-4 text-zinc-900">Recent Programs</h2>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left">
|
||||
<thead>
|
||||
<tr className="border-b border-zinc-100 text-zinc-500 text-sm uppercasetracking-wider">
|
||||
<th className="pb-3 font-medium">Program Name</th>
|
||||
<th className="pb-3 font-medium">Client</th>
|
||||
<th className="pb-3 font-medium">Created At</th>
|
||||
<th className="pb-3 font-medium">Exercises</th>
|
||||
<th className="pb-3 font-medium">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-zinc-100">
|
||||
{stats.recentMenus.map((menu: any) => (
|
||||
<tr key={menu.id} className="group hover:bg-zinc-50 transition-colors">
|
||||
<td className="py-4 font-medium text-zinc-900">{menu.name}</td>
|
||||
<td className="py-4 text-zinc-600 font-medium">
|
||||
{menu.assigned_client ? (
|
||||
<span className="bg-blue-50 text-blue-700 px-2 py-1 rounded-md text-xs border border-blue-100">
|
||||
{menu.assigned_client.name}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-zinc-400 text-xs italic">Unassigned</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="py-4 text-zinc-500 text-sm">
|
||||
{new Date(menu.created_at).toLocaleDateString()}
|
||||
</td>
|
||||
<td className="py-4 text-zinc-500 text-sm">
|
||||
{(() => {
|
||||
if (!menu.exercises) return 0;
|
||||
if (Array.isArray(menu.exercises)) return menu.exercises.length;
|
||||
try { return JSON.parse(menu.exercises as string).length; } catch { return 0; }
|
||||
})()} exercises
|
||||
</td>
|
||||
<td className="py-4">
|
||||
<Link
|
||||
href={`/coach/menu/${menu.id}`}
|
||||
className="text-primary hover:text-blue-700 text-sm font-bold flex items-center gap-1"
|
||||
>
|
||||
View Details
|
||||
</Link>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{stats.recentMenus.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={4} className="py-8 text-center text-zinc-400 italic">No programs created yet.</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</motion.div>
|
||||
|
||||
{/* Recent Recaps List */}
|
||||
<motion.div
|
||||
className="mt-6 bg-white border border-zinc-200 rounded-xl p-6 shadow-sm"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.7 }}
|
||||
>
|
||||
<h2 className="text-xl font-bold mb-4 text-zinc-900">Recent Activity Reports</h2>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left">
|
||||
<thead>
|
||||
<tr className="border-b border-zinc-100 text-zinc-500 text-sm uppercasetracking-wider">
|
||||
<th className="pb-3 font-medium">Date</th>
|
||||
<th className="pb-3 font-medium">Client</th>
|
||||
<th className="pb-3 font-medium">Program</th>
|
||||
<th className="pb-3 font-medium">Status</th>
|
||||
<th className="pb-3 font-medium">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-zinc-100">
|
||||
{stats.recentRecaps.map((recap: any) => (
|
||||
<tr key={recap.id} className="group hover:bg-zinc-50 transition-colors">
|
||||
<td className="py-4 text-zinc-500 text-sm">
|
||||
{new Date(recap.completed_at).toLocaleString()}
|
||||
</td>
|
||||
<td className="py-4 font-medium text-zinc-900">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-6 h-6 rounded-full bg-blue-100 flex items-center justify-center text-blue-600 text-xs font-bold">
|
||||
{recap.user?.name ? recap.user.name[0] : '?'}
|
||||
</div>
|
||||
{recap.user?.name || 'Unknown'}
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-4 text-zinc-600 text-sm">
|
||||
{recap.training_menus?.name || `Menu #${recap.menu_id}`}
|
||||
</td>
|
||||
<td className="py-4">
|
||||
<span className="bg-green-100 text-green-700 px-3 py-1 rounded-full text-xs font-bold">
|
||||
COMPLETED
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-4">
|
||||
<Link
|
||||
href={`/coach/recap/${recap.id}`}
|
||||
className="text-primary hover:text-blue-700 text-sm font-bold flex items-center gap-1"
|
||||
>
|
||||
View Report
|
||||
</Link>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{stats.recentRecaps.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={4} className="py-8 text-center text-zinc-400 italic">No activity recorded yet.</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StatsCard({ title, value, icon, variant }: any) {
|
||||
return (
|
||||
<motion.div
|
||||
variants={variant}
|
||||
className="p-8 rounded-3xl bg-white border border-zinc-200 hover:border-primary/50 transition-all group relative overflow-hidden shadow-sm hover:shadow-md"
|
||||
>
|
||||
<div className="absolute top-0 right-0 p-8 opacity-10 group-hover:opacity-100 group-hover:scale-110 transition-all duration-500 grayscale group-hover:grayscale-0">
|
||||
{icon}
|
||||
</div>
|
||||
<div className="relative z-10">
|
||||
<h3 className="text-zinc-500 text-xs font-bold uppercase tracking-[0.2em] mb-3">{title}</h3>
|
||||
<div className="text-5xl font-light text-zinc-900 tracking-tighter">{value}</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
150
app/coach/menu/[id]/page.tsx
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
|
||||
'use client';
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { ArrowLeft, Calendar, Dumbbell, Clock, Repeat } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function MenuDetailPage() {
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
const [menu, setMenu] = useState<any>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (!params.id) return;
|
||||
|
||||
async function fetchMenu() {
|
||||
try {
|
||||
const res = await fetch(`/api/menus/${params.id}`);
|
||||
if (!res.ok) throw new Error('Failed to fetch');
|
||||
const data = await res.json();
|
||||
|
||||
// Parse exercises if string
|
||||
if (typeof data.exercises === 'string') {
|
||||
data.exercises = JSON.parse(data.exercises);
|
||||
}
|
||||
setMenu(data);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
// Optionally redirect or show error
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
fetchMenu();
|
||||
}, [params.id]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-zinc-50 flex items-center justify-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!menu) {
|
||||
return (
|
||||
<div className="min-h-screen bg-zinc-50 flex flex-col items-center justify-center text-zinc-500">
|
||||
<p className="mb-4">Menu not found.</p>
|
||||
<Link href="/coach/dashboard" className="text-primary hover:underline">Return to Dashboard</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background text-foreground p-8 font-sans">
|
||||
<header className="max-w-4xl mx-auto mb-10">
|
||||
<Link
|
||||
href="/coach/dashboard"
|
||||
className="inline-flex items-center gap-2 text-zinc-500 hover:text-primary transition-colors mb-6 group"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4 group-hover:-translate-x-1 transition-transform" />
|
||||
Back to Dashboard
|
||||
</Link>
|
||||
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-4xl font-light text-zinc-900 tracking-tight mb-2">
|
||||
{menu.name}
|
||||
</h1>
|
||||
<div className="flex items-center gap-4 text-sm text-zinc-500">
|
||||
<span className="flex items-center gap-1">
|
||||
<Calendar className="w-4 h-4" />
|
||||
{new Date(menu.created_at).toLocaleDateString()}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Dumbbell className="w-4 h-4" />
|
||||
{menu.exercises?.length || 0} Exercises
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => window.print()}
|
||||
className="px-6 py-2 bg-white border border-zinc-200 text-zinc-700 rounded-lg hover:bg-zinc-50 text-sm font-medium shadow-sm transition-all"
|
||||
>
|
||||
Print Program
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="max-w-4xl mx-auto">
|
||||
<div className="bg-white rounded-2xl shadow-sm border border-zinc-200 overflow-hidden">
|
||||
<div className="p-6 border-b border-zinc-100 bg-zinc-50/50 flex justify-between items-center">
|
||||
<h2 className="font-bold text-zinc-700 uppercase text-xs tracking-widest">Exercise Schedule</h2>
|
||||
<span className="bg-green-100 text-green-700 px-3 py-1 rounded-full text-xs font-bold">ACTIVE</span>
|
||||
</div>
|
||||
|
||||
<div className="divide-y divide-zinc-100">
|
||||
{menu.exercises?.map((ex: any, idx: number) => (
|
||||
<div key={idx} className="p-6 hover:bg-zinc-50 transition-colors flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-10 h-10 rounded-full bg-blue-50 text-primary flex items-center justify-center font-bold text-lg shrink-0">
|
||||
{idx + 1}
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-zinc-900">{ex.name}</h3>
|
||||
<p className="text-sm text-zinc-500 mt-1">Target Muscles: General</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-8 text-sm">
|
||||
<div className="flex flex-col items-center">
|
||||
<span className="text-zinc-400 text-xs uppercase tracking-wider font-bold mb-1">Sets</span>
|
||||
<span className="text-xl font-light text-zinc-900">{ex.sets}</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center">
|
||||
<span className="text-zinc-400 text-xs uppercase tracking-wider font-bold mb-1">Reps</span>
|
||||
<span className="text-xl font-light text-zinc-900">{ex.reps}</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center">
|
||||
<span className="text-zinc-400 text-xs uppercase tracking-wider font-bold mb-1">Weight</span>
|
||||
<span className="text-xl font-light text-zinc-900">{ex.weight}kg</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center w-20">
|
||||
<span className="text-zinc-400 text-xs uppercase tracking-wider font-bold mb-1 flex items-center gap-1"><Clock className="w-3 h-3"/> Rest</span>
|
||||
<span className="text-xl font-light text-zinc-900">{ex.rest_time_seconds || 0}s</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div className="p-6 bg-blue-50 rounded-xl border border-blue-100">
|
||||
<h4 className="text-blue-900 font-bold mb-2">Coach Notes</h4>
|
||||
<p className="text-blue-700/80 text-sm">Ensure client maintains proper form during the eccentric phase of each movement.</p>
|
||||
</div>
|
||||
<div className="p-6 bg-orange-50 rounded-xl border border-orange-100">
|
||||
<h4 className="text-orange-900 font-bold mb-2">Safety Protocols</h4>
|
||||
<p className="text-orange-700/80 text-sm">Stop immediately if pain is reported in joints. Monitor heart rate variations.</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
350
app/coach/menu/new/page.tsx
Normal file
|
|
@ -0,0 +1,350 @@
|
|||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Plus, Trash2, Save, ArrowLeft, Copy, Layers, GripVertical } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { useAuth, AuthProvider } from '@/lib/auth';
|
||||
|
||||
interface ExerciseItem {
|
||||
id: string;
|
||||
name: string;
|
||||
reps: number;
|
||||
weight: number;
|
||||
rest_time_seconds: number;
|
||||
}
|
||||
|
||||
interface RoundData {
|
||||
id: string;
|
||||
exercises: ExerciseItem[];
|
||||
}
|
||||
|
||||
export default function CreateMenuPageWrap() {
|
||||
return (
|
||||
<AuthProvider>
|
||||
<CreateMenuPage />
|
||||
</AuthProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function CreateMenuPage() {
|
||||
const router = useRouter();
|
||||
const { user } = useAuth();
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [menuName, setMenuName] = useState('');
|
||||
|
||||
// Round-Based State
|
||||
const [rounds, setRounds] = useState<RoundData[]>([
|
||||
{
|
||||
id: 'round-1',
|
||||
exercises: [
|
||||
{ id: 'ex-1', name: 'Squat', reps: 10, weight: 20, rest_time_seconds: 30 }
|
||||
]
|
||||
}
|
||||
]);
|
||||
|
||||
const [clients, setClients] = useState<any[]>([]);
|
||||
const [selectedClient, setSelectedClient] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
fetch(`/api/users?coachId=${encodeURIComponent(user.id)}`)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (Array.isArray(data)) setClients(data);
|
||||
});
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
// --- Actions ---
|
||||
|
||||
const addRound = () => {
|
||||
setRounds([...rounds, {
|
||||
id: Math.random().toString(36).substr(2, 9),
|
||||
exercises: []
|
||||
}]);
|
||||
};
|
||||
|
||||
const duplicateRound = (sourceIndex: number) => {
|
||||
const source = rounds[sourceIndex];
|
||||
const newExercises = source.exercises.map(ex => ({
|
||||
...ex,
|
||||
id: Math.random().toString(36).substr(2, 9)
|
||||
}));
|
||||
|
||||
setRounds([...rounds, {
|
||||
id: Math.random().toString(36).substr(2, 9),
|
||||
exercises: newExercises
|
||||
}]);
|
||||
};
|
||||
|
||||
const removeRound = (index: number) => {
|
||||
setRounds(rounds.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
const addExerciseToRound = (roundIndex: number) => {
|
||||
const newRounds = [...rounds];
|
||||
newRounds[roundIndex].exercises.push({
|
||||
id: Math.random().toString(36).substr(2, 9),
|
||||
name: 'Bicep Curl',
|
||||
reps: 10,
|
||||
weight: 10,
|
||||
rest_time_seconds: 30
|
||||
});
|
||||
setRounds(newRounds);
|
||||
};
|
||||
|
||||
const removeExerciseFromRound = (roundIndex: number, exIndex: number) => {
|
||||
const newRounds = [...rounds];
|
||||
newRounds[roundIndex].exercises = newRounds[roundIndex].exercises.filter((_, i) => i !== exIndex);
|
||||
setRounds(newRounds);
|
||||
};
|
||||
|
||||
const updateExercise = (roundIndex: number, exIndex: number, field: keyof ExerciseItem, value: any) => {
|
||||
const newRounds = [...rounds];
|
||||
newRounds[roundIndex].exercises[exIndex] = {
|
||||
...newRounds[roundIndex].exercises[exIndex],
|
||||
[field]: value
|
||||
};
|
||||
setRounds(newRounds);
|
||||
};
|
||||
|
||||
// --- Submit Logic (Flattening) ---
|
||||
|
||||
// We assume the user creates rounds sequentially: Set 1, Set 2.
|
||||
// So distinct Sets of "Squat" will imply Set 1, Set 2 logic naturally in the list.
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
if (!selectedClient) {
|
||||
alert("Please select a client.");
|
||||
setIsSubmitting(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// FLATTEN ROUNDS
|
||||
const flatList: any[] = [];
|
||||
const counts: Record<string, number> = {};
|
||||
const totals: Record<string, number> = {};
|
||||
|
||||
// 1. Calculate Totals (First Pass)
|
||||
rounds.forEach(round => {
|
||||
round.exercises.forEach(ex => {
|
||||
totals[ex.name] = (totals[ex.name] || 0) + 1;
|
||||
});
|
||||
});
|
||||
|
||||
// 2. Flatten and Assign Indices
|
||||
rounds.forEach((round, roundIndex) => {
|
||||
round.exercises.forEach(ex => {
|
||||
counts[ex.name] = (counts[ex.name] || 0) + 1;
|
||||
|
||||
flatList.push({
|
||||
name: ex.name,
|
||||
reps: ex.reps,
|
||||
weight: ex.weight,
|
||||
rest_time_seconds: ex.rest_time_seconds,
|
||||
// This corresponds to "Which instance of Squat is this?" -> Set Number
|
||||
set_index: counts[ex.name],
|
||||
total_sets: totals[ex.name]
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const res = await fetch('/api/menus', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-user-id': user?.id || ''
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: menuName,
|
||||
exercises: flatList,
|
||||
client_id: selectedClient
|
||||
})
|
||||
});
|
||||
|
||||
if (!res.ok) throw new Error('Failed');
|
||||
router.push('/coach/dashboard');
|
||||
} catch (error) {
|
||||
alert('Error creating menu');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const EXERCISE_OPTIONS = [
|
||||
"Bicep Curl", "Hammer Curl", "Squat", "Deadlift", "Lunges", "Overhead Press", "Lateral Raises"
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background text-foreground p-8 font-sans pb-32">
|
||||
<header className="max-w-4xl mx-auto mb-10 flex items-center gap-6">
|
||||
<Link href="/coach/dashboard" className="p-3 bg-white hover:bg-zinc-100 rounded-full transition-colors border border-zinc-200 shadow-sm">
|
||||
<ArrowLeft className="w-5 h-5 text-zinc-600" />
|
||||
</Link>
|
||||
<div>
|
||||
<h1 className="text-3xl font-light text-zinc-900 tracking-wide">
|
||||
Program <span className="font-bold text-primary">Composer</span>
|
||||
</h1>
|
||||
<p className="text-zinc-500 text-sm mt-1">Design training blocks set-by-set.</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<form onSubmit={handleSubmit} className="max-w-4xl mx-auto space-y-8">
|
||||
{/* Meta Info */}
|
||||
<div className="bg-white p-6 rounded-2xl border border-zinc-200 shadow-sm">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-zinc-400 uppercase tracking-widest mb-2">Program Name</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="e.g. Hypertrophy A"
|
||||
value={menuName}
|
||||
onChange={(e) => setMenuName(e.target.value)}
|
||||
className="w-full text-xl font-bold border-b-2 border-zinc-100 focus:border-primary outline-none py-2 transition-colors placeholder:font-normal"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-zinc-400 uppercase tracking-widest mb-2">Assign To Client</label>
|
||||
<select
|
||||
value={selectedClient}
|
||||
onChange={(e) => setSelectedClient(e.target.value)}
|
||||
className="w-full text-lg border-b-2 border-zinc-100 focus:border-primary outline-none py-2 bg-transparent transition-colors"
|
||||
>
|
||||
<option value="" disabled>Select a client...</option>
|
||||
{clients.map(c => (
|
||||
<option key={c.id} value={c.id}>{c.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Rounds */}
|
||||
<div className="grid grid-cols-1 gap-6">
|
||||
{rounds.map((round, roundIndex) => (
|
||||
<motion.div
|
||||
key={round.id}
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
className="bg-zinc-50 border border-zinc-200 rounded-3xl p-6 md:p-8 relative shadow-sm group/round"
|
||||
>
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h3 className="text-xl font-black text-zinc-300 uppercase tracking-tighter flex items-center gap-2">
|
||||
<span className="text-4xl text-zinc-200">#{ (roundIndex + 1).toString().padStart(2, '0') }</span>
|
||||
SET
|
||||
</h3>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => duplicateRound(roundIndex)}
|
||||
className="p-2 text-zinc-400 hover:text-blue-500 hover:bg-blue-50 rounded-xl transition-all"
|
||||
title="Duplicate this Set"
|
||||
>
|
||||
<Copy className="w-5 h-5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeRound(roundIndex)}
|
||||
disabled={rounds.length === 1}
|
||||
className="p-2 text-zinc-300 hover:text-red-500 hover:bg-red-50 rounded-xl transition-all disabled:opacity-0"
|
||||
>
|
||||
<Trash2 className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Exercises in Round */}
|
||||
<div className="space-y-3">
|
||||
{round.exercises.map((ex, exIndex) => (
|
||||
<div key={ex.id} className="bg-white p-4 rounded-xl shadow-sm border border-zinc-100 flex flex-col md:flex-row gap-4 items-center group/ex">
|
||||
<div className="flex-1 w-full">
|
||||
<label className="block text-[10px] font-bold text-zinc-300 uppercase mb-1 md:hidden">Exercise</label>
|
||||
<select
|
||||
value={ex.name}
|
||||
onChange={(e) => updateExercise(roundIndex, exIndex, 'name', e.target.value)}
|
||||
className="w-full bg-transparent font-bold text-zinc-900 focus:outline-none cursor-pointer"
|
||||
>
|
||||
{EXERCISE_OPTIONS.map(opt => <option key={opt} value={opt}>{opt}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex gap-2 w-full md:w-auto">
|
||||
<div>
|
||||
<label className="block text-[10px] font-bold text-zinc-300 uppercase mb-1 text-center">Reps</label>
|
||||
<input
|
||||
type="number"
|
||||
value={isNaN(ex.reps) ? '' : ex.reps}
|
||||
onChange={(e) => updateExercise(roundIndex, exIndex, 'reps', parseInt(e.target.value))}
|
||||
className="w-full md:w-16 bg-zinc-50 border border-zinc-100 rounded-lg px-2 py-1.5 text-center font-mono text-sm focus:border-primary outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-[10px] font-bold text-zinc-300 uppercase mb-1 text-center">Kg</label>
|
||||
<input
|
||||
type="number"
|
||||
value={isNaN(ex.weight) ? '' : ex.weight}
|
||||
onChange={(e) => updateExercise(roundIndex, exIndex, 'weight', parseFloat(e.target.value))}
|
||||
className="w-full md:w-16 bg-zinc-50 border border-zinc-100 rounded-lg px-2 py-1.5 text-center font-mono text-sm focus:border-primary outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-[10px] font-bold text-zinc-300 uppercase mb-1 text-center">Rest</label>
|
||||
<input
|
||||
type="number"
|
||||
value={isNaN(ex.rest_time_seconds) ? '' : ex.rest_time_seconds}
|
||||
onChange={(e) => updateExercise(roundIndex, exIndex, 'rest_time_seconds', parseFloat(e.target.value))}
|
||||
className="w-full md:w-16 bg-zinc-50 border border-zinc-100 rounded-lg px-2 py-1.5 text-center font-mono text-sm focus:border-primary outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeExerciseFromRound(roundIndex, exIndex)}
|
||||
className="p-2 text-zinc-300 hover:text-red-500 transition-colors"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => addExerciseToRound(roundIndex)}
|
||||
className="w-full py-3 border-2 border-dashed border-zinc-200 rounded-xl text-zinc-400 hover:text-primary hover:border-primary hover:bg-blue-50/50 transition-all font-bold text-xs uppercase tracking-widest flex items-center justify-center gap-2"
|
||||
>
|
||||
<Plus className="w-4 h-4" /> Add Exercise
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center py-6">
|
||||
<button
|
||||
type="button"
|
||||
onClick={addRound}
|
||||
className="bg-zinc-100 hover:bg-zinc-200 text-zinc-800 px-8 py-3 rounded-full font-bold shadow-sm hover:scale-105 transition-all flex items-center gap-2"
|
||||
>
|
||||
<Plus className="w-5 h-5" /> ADD NEW SET
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="fixed bottom-0 left-0 right-0 p-6 bg-white/80 backdrop-blur-lg border-t border-zinc-200 flex justify-center z-50">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="w-full max-w-md bg-primary hover:bg-primary/90 text-black font-black uppercase tracking-widest py-4 rounded-2xl shadow-[0_0_30px_-5px_var(--color-primary)] transform transition-all active:scale-95 disabled:opacity-50 flex items-center justify-center gap-3"
|
||||
>
|
||||
{isSubmitting ? 'Saving...' : 'Deploy Program'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
156
app/coach/recap/[id]/page.tsx
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
|
||||
'use client';
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { ArrowLeft, Calendar, User, Clock, CheckCircle } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function RecapDetailPage() {
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
const [recap, setRecap] = useState<any>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (!params.id) return;
|
||||
|
||||
async function fetchRecap() {
|
||||
try {
|
||||
const res = await fetch(`/api/recap/${params.id}`);
|
||||
if (!res.ok) throw new Error('Failed to fetch');
|
||||
const data = await res.json();
|
||||
|
||||
// Parse summary/exercises if string (though usually JSON in Prisma)
|
||||
// Assuming Prisma handles JSON parsing automatically for `details` field
|
||||
setRecap(data);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
fetchRecap();
|
||||
}, [params.id]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-zinc-50 flex items-center justify-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!recap) {
|
||||
return (
|
||||
<div className="min-h-screen bg-zinc-50 flex flex-col items-center justify-center text-zinc-500">
|
||||
<p className="mb-4">Report not found.</p>
|
||||
<Link href="/coach/dashboard" className="text-primary hover:underline">Return to Dashboard</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const { summary, training_menus } = recap;
|
||||
const exercises = summary?.exercises || [];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background text-foreground p-8 font-sans">
|
||||
<header className="max-w-4xl mx-auto mb-10">
|
||||
<Link
|
||||
href="/coach/dashboard"
|
||||
className="inline-flex items-center gap-2 text-zinc-500 hover:text-primary transition-colors mb-6 group"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4 group-hover:-translate-x-1 transition-transform" />
|
||||
Back to Dashboard
|
||||
</Link>
|
||||
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-4xl font-light text-zinc-900 tracking-tight mb-2">
|
||||
Session Report <span className="text-zinc-400">#{recap.id}</span>
|
||||
</h1>
|
||||
<div className="flex items-center gap-4 text-sm text-zinc-500">
|
||||
<span className="flex items-center gap-1">
|
||||
<Calendar className="w-4 h-4" />
|
||||
{new Date(recap.completed_at).toLocaleDateString()}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<User className="w-4 h-4" />
|
||||
{training_menus ? training_menus.name : `Menu #${recap.menu_id}`}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Clock className="w-4 h-4" />
|
||||
{new Date(recap.completed_at).toLocaleTimeString()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="max-w-4xl mx-auto space-y-8">
|
||||
{/* Performance Summary */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="bg-white p-6 rounded-2xl shadow-sm border border-zinc-200">
|
||||
<h2 className="text-zinc-400 text-xs font-bold uppercase tracking-widest mb-4">Completion Status</h2>
|
||||
<div className="flex items-center gap-3">
|
||||
<CheckCircle className="w-10 h-10 text-green-500" />
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-zinc-900">Completed</div>
|
||||
<div className="text-sm text-zinc-500">All exercises finished</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Placeholder for future detailed analysis */}
|
||||
<div className="bg-white p-6 rounded-2xl shadow-sm border border-zinc-200 opacity-60 grayscale">
|
||||
<h2 className="text-zinc-400 text-xs font-bold uppercase tracking-widest mb-4">Total Load</h2>
|
||||
<div className="text-3xl font-light text-zinc-800">-- kg</div>
|
||||
<div className="text-xs text-blue-500 font-bold uppercase mt-2">Analysis Coming Soon</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-2xl shadow-sm border border-zinc-200 overflow-hidden">
|
||||
<div className="p-6 border-b border-zinc-100 bg-zinc-50/50">
|
||||
<h2 className="font-bold text-zinc-700 uppercase text-xs tracking-widest">Exercise Log</h2>
|
||||
</div>
|
||||
|
||||
<div className="divide-y divide-zinc-100">
|
||||
{exercises.map((ex: any, idx: number) => (
|
||||
<div key={idx} className="p-6 flex flex-col md:flex-row md:items-center justify-between gap-4 group hover:bg-zinc-50 transition-colors">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-8 h-8 rounded-full bg-zinc-100 text-zinc-400 flex items-center justify-center font-bold text-sm shrink-0">
|
||||
{idx + 1}
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-lg font-bold text-zinc-900">{ex.name}</h3>
|
||||
<span className="px-2 py-0.5 rounded text-[10px] font-bold bg-blue-50 text-blue-600 border border-blue-100 uppercase tracking-wide">
|
||||
Set {ex.set_index || '?'}/{ex.total_sets || '?'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex gap-2 text-xs font-medium uppercase tracking-wider text-zinc-400 mt-1">
|
||||
<span>{ex.reps} Reps</span>
|
||||
<span>•</span>
|
||||
<span>{ex.weight} kg</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="px-3 py-1 bg-green-50 text-green-700 rounded-lg text-xs font-bold uppercase border border-green-100">
|
||||
Target Met
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{exercises.length === 0 && (
|
||||
<div className="p-8 text-center text-zinc-400 italic">
|
||||
No exercise details available for this session.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
BIN
app/favicon.ico
Normal file
|
After Width: | Height: | Size: 25 KiB |
39
app/generated/client/browser.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
// @ts-nocheck
|
||||
/*
|
||||
* This file should be your main import to use Prisma-related types and utilities in a browser.
|
||||
* Use it to get access to models, enums, and input types.
|
||||
*
|
||||
* This file does not contain a `PrismaClient` class, nor several other helpers that are intended as server-side only.
|
||||
* See `client.ts` for the standard, server-side entry point.
|
||||
*
|
||||
* 🟢 You can import this file directly.
|
||||
*/
|
||||
|
||||
import * as Prisma from './internal/prismaNamespaceBrowser.js'
|
||||
export { Prisma }
|
||||
export * as $Enums from './enums.js'
|
||||
export * from './enums.js';
|
||||
/**
|
||||
* Model users
|
||||
*
|
||||
*/
|
||||
export type users = Prisma.usersModel
|
||||
/**
|
||||
* Model activity_logs
|
||||
*
|
||||
*/
|
||||
export type activity_logs = Prisma.activity_logsModel
|
||||
/**
|
||||
* Model training_menus
|
||||
*
|
||||
*/
|
||||
export type training_menus = Prisma.training_menusModel
|
||||
/**
|
||||
* Model user_recaps
|
||||
*
|
||||
*/
|
||||
export type user_recaps = Prisma.user_recapsModel
|
||||
66
app/generated/client/client.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
// @ts-nocheck
|
||||
/*
|
||||
* This file should be your main import to use Prisma. Through it you get access to all the models, enums, and input types.
|
||||
* If you're looking for something you can import in the client-side of your application, please refer to the `browser.ts` file instead.
|
||||
*
|
||||
* 🟢 You can import this file directly.
|
||||
*/
|
||||
|
||||
import * as process from 'node:process'
|
||||
import * as path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
globalThis['__dirname'] = path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
import * as runtime from "@prisma/client/runtime/library"
|
||||
import * as $Enums from "./enums.js"
|
||||
import * as $Class from "./internal/class.js"
|
||||
import * as Prisma from "./internal/prismaNamespace.js"
|
||||
|
||||
export * as $Enums from './enums.js'
|
||||
export * from "./enums.js"
|
||||
/**
|
||||
* ## Prisma Client
|
||||
*
|
||||
* Type-safe database client for TypeScript
|
||||
* @example
|
||||
* ```
|
||||
* const prisma = new PrismaClient()
|
||||
* // Fetch zero or more Users
|
||||
* const users = await prisma.users.findMany()
|
||||
* ```
|
||||
*
|
||||
* Read more in our [docs](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client).
|
||||
*/
|
||||
export const PrismaClient = $Class.getPrismaClientClass(__dirname)
|
||||
export type PrismaClient<LogOpts extends Prisma.LogLevel = never, OmitOpts extends Prisma.PrismaClientOptions["omit"] = Prisma.PrismaClientOptions["omit"], ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = $Class.PrismaClient<LogOpts, OmitOpts, ExtArgs>
|
||||
export { Prisma }
|
||||
|
||||
|
||||
// file annotations for bundling tools to include these files
|
||||
path.join(__dirname, "query_engine-windows.dll.node")
|
||||
path.join(process.cwd(), "app/generated/client/query_engine-windows.dll.node")
|
||||
|
||||
/**
|
||||
* Model users
|
||||
*
|
||||
*/
|
||||
export type users = Prisma.usersModel
|
||||
/**
|
||||
* Model activity_logs
|
||||
*
|
||||
*/
|
||||
export type activity_logs = Prisma.activity_logsModel
|
||||
/**
|
||||
* Model training_menus
|
||||
*
|
||||
*/
|
||||
export type training_menus = Prisma.training_menusModel
|
||||
/**
|
||||
* Model user_recaps
|
||||
*
|
||||
*/
|
||||
export type user_recaps = Prisma.user_recapsModel
|
||||
405
app/generated/client/commonInputTypes.ts
Normal file
|
|
@ -0,0 +1,405 @@
|
|||
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
// @ts-nocheck
|
||||
/*
|
||||
* This file exports various common sort, input & filter types that are not directly linked to a particular model.
|
||||
*
|
||||
* 🟢 You can import this file directly.
|
||||
*/
|
||||
|
||||
import type * as runtime from "@prisma/client/runtime/library"
|
||||
import * as $Enums from "./enums.js"
|
||||
import type * as Prisma from "./internal/prismaNamespace.js"
|
||||
|
||||
|
||||
export type StringFilter<$PrismaModel = never> = {
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
|
||||
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
mode?: Prisma.QueryMode
|
||||
not?: Prisma.NestedStringFilter<$PrismaModel> | string
|
||||
}
|
||||
|
||||
export type StringNullableFilter<$PrismaModel = never> = {
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
||||
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
|
||||
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
mode?: Prisma.QueryMode
|
||||
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null
|
||||
}
|
||||
|
||||
export type DateTimeNullableFilter<$PrismaModel = never> = {
|
||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
|
||||
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
|
||||
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
|
||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> | Date | string | null
|
||||
}
|
||||
|
||||
export type SortOrderInput = {
|
||||
sort: Prisma.SortOrder
|
||||
nulls?: Prisma.NullsOrder
|
||||
}
|
||||
|
||||
export type StringWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
|
||||
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
mode?: Prisma.QueryMode
|
||||
not?: Prisma.NestedStringWithAggregatesFilter<$PrismaModel> | string
|
||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedStringFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedStringFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type StringNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
||||
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
|
||||
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
mode?: Prisma.QueryMode
|
||||
not?: Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel> | string | null
|
||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type DateTimeNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
|
||||
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
|
||||
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
|
||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedDateTimeNullableWithAggregatesFilter<$PrismaModel> | Date | string | null
|
||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type IntFilter<$PrismaModel = never> = {
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
|
||||
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
|
||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedIntFilter<$PrismaModel> | number
|
||||
}
|
||||
|
||||
export type JsonNullableFilter<$PrismaModel = never> =
|
||||
| Prisma.PatchUndefined<
|
||||
Prisma.Either<Required<JsonNullableFilterBase<$PrismaModel>>, Exclude<keyof Required<JsonNullableFilterBase<$PrismaModel>>, 'path'>>,
|
||||
Required<JsonNullableFilterBase<$PrismaModel>>
|
||||
>
|
||||
| Prisma.OptionalFlat<Omit<Required<JsonNullableFilterBase<$PrismaModel>>, 'path'>>
|
||||
|
||||
export type JsonNullableFilterBase<$PrismaModel = never> = {
|
||||
equals?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
|
||||
path?: string[]
|
||||
mode?: Prisma.QueryMode | Prisma.EnumQueryModeFieldRefInput<$PrismaModel>
|
||||
string_contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
string_starts_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
string_ends_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
array_starts_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
|
||||
array_ends_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
|
||||
array_contains?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
|
||||
lt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
|
||||
lte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
|
||||
gt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
|
||||
gte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
|
||||
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
|
||||
}
|
||||
|
||||
export type IntWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
|
||||
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
|
||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedIntWithAggregatesFilter<$PrismaModel> | number
|
||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_avg?: Prisma.NestedFloatFilter<$PrismaModel>
|
||||
_sum?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type JsonNullableWithAggregatesFilter<$PrismaModel = never> =
|
||||
| Prisma.PatchUndefined<
|
||||
Prisma.Either<Required<JsonNullableWithAggregatesFilterBase<$PrismaModel>>, Exclude<keyof Required<JsonNullableWithAggregatesFilterBase<$PrismaModel>>, 'path'>>,
|
||||
Required<JsonNullableWithAggregatesFilterBase<$PrismaModel>>
|
||||
>
|
||||
| Prisma.OptionalFlat<Omit<Required<JsonNullableWithAggregatesFilterBase<$PrismaModel>>, 'path'>>
|
||||
|
||||
export type JsonNullableWithAggregatesFilterBase<$PrismaModel = never> = {
|
||||
equals?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
|
||||
path?: string[]
|
||||
mode?: Prisma.QueryMode | Prisma.EnumQueryModeFieldRefInput<$PrismaModel>
|
||||
string_contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
string_starts_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
string_ends_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
array_starts_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
|
||||
array_ends_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
|
||||
array_contains?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
|
||||
lt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
|
||||
lte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
|
||||
gt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
|
||||
gte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
|
||||
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
|
||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedJsonNullableFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedJsonNullableFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type IntNullableFilter<$PrismaModel = never> = {
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
|
||||
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
|
||||
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
|
||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedIntNullableFilter<$PrismaModel> | number | null
|
||||
}
|
||||
|
||||
export type IntNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
|
||||
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
|
||||
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
|
||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedIntNullableWithAggregatesFilter<$PrismaModel> | number | null
|
||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
_avg?: Prisma.NestedFloatNullableFilter<$PrismaModel>
|
||||
_sum?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type NestedStringFilter<$PrismaModel = never> = {
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
|
||||
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedStringFilter<$PrismaModel> | string
|
||||
}
|
||||
|
||||
export type NestedStringNullableFilter<$PrismaModel = never> = {
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
||||
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
|
||||
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null
|
||||
}
|
||||
|
||||
export type NestedDateTimeNullableFilter<$PrismaModel = never> = {
|
||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
|
||||
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
|
||||
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
|
||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> | Date | string | null
|
||||
}
|
||||
|
||||
export type NestedStringWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
|
||||
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedStringWithAggregatesFilter<$PrismaModel> | string
|
||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedStringFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedStringFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type NestedIntFilter<$PrismaModel = never> = {
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
|
||||
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
|
||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedIntFilter<$PrismaModel> | number
|
||||
}
|
||||
|
||||
export type NestedStringNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
||||
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
|
||||
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel> | string | null
|
||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type NestedIntNullableFilter<$PrismaModel = never> = {
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
|
||||
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
|
||||
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
|
||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedIntNullableFilter<$PrismaModel> | number | null
|
||||
}
|
||||
|
||||
export type NestedDateTimeNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
|
||||
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
|
||||
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
|
||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedDateTimeNullableWithAggregatesFilter<$PrismaModel> | Date | string | null
|
||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type NestedIntWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
|
||||
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
|
||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedIntWithAggregatesFilter<$PrismaModel> | number
|
||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_avg?: Prisma.NestedFloatFilter<$PrismaModel>
|
||||
_sum?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type NestedFloatFilter<$PrismaModel = never> = {
|
||||
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||
in?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel>
|
||||
notIn?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel>
|
||||
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedFloatFilter<$PrismaModel> | number
|
||||
}
|
||||
|
||||
export type NestedJsonNullableFilter<$PrismaModel = never> =
|
||||
| Prisma.PatchUndefined<
|
||||
Prisma.Either<Required<NestedJsonNullableFilterBase<$PrismaModel>>, Exclude<keyof Required<NestedJsonNullableFilterBase<$PrismaModel>>, 'path'>>,
|
||||
Required<NestedJsonNullableFilterBase<$PrismaModel>>
|
||||
>
|
||||
| Prisma.OptionalFlat<Omit<Required<NestedJsonNullableFilterBase<$PrismaModel>>, 'path'>>
|
||||
|
||||
export type NestedJsonNullableFilterBase<$PrismaModel = never> = {
|
||||
equals?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
|
||||
path?: string[]
|
||||
mode?: Prisma.QueryMode | Prisma.EnumQueryModeFieldRefInput<$PrismaModel>
|
||||
string_contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
string_starts_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
string_ends_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
array_starts_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
|
||||
array_ends_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
|
||||
array_contains?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
|
||||
lt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
|
||||
lte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
|
||||
gt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
|
||||
gte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
|
||||
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
|
||||
}
|
||||
|
||||
export type NestedIntNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
|
||||
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
|
||||
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
|
||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedIntNullableWithAggregatesFilter<$PrismaModel> | number | null
|
||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
_avg?: Prisma.NestedFloatNullableFilter<$PrismaModel>
|
||||
_sum?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type NestedFloatNullableFilter<$PrismaModel = never> = {
|
||||
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel> | null
|
||||
in?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> | null
|
||||
notIn?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> | null
|
||||
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedFloatNullableFilter<$PrismaModel> | number | null
|
||||
}
|
||||
|
||||
|
||||
15
app/generated/client/enums.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
// @ts-nocheck
|
||||
/*
|
||||
* This file exports all enum related types from the schema.
|
||||
*
|
||||
* 🟢 You can import this file directly.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
// This file is empty because there are no enums in the schema.
|
||||
export {}
|
||||
248
app/generated/client/internal/class.ts
Normal file
1066
app/generated/client/internal/prismaNamespace.ts
Normal file
159
app/generated/client/internal/prismaNamespaceBrowser.ts
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
// @ts-nocheck
|
||||
/*
|
||||
* WARNING: This is an internal file that is subject to change!
|
||||
*
|
||||
* 🛑 Under no circumstances should you import this file directly! 🛑
|
||||
*
|
||||
* All exports from this file are wrapped under a `Prisma` namespace object in the browser.ts file.
|
||||
* While this enables partial backward compatibility, it is not part of the stable public API.
|
||||
*
|
||||
* If you are looking for your Models, Enums, and Input Types, please import them from the respective
|
||||
* model files in the `model` directory!
|
||||
*/
|
||||
|
||||
import * as runtime from "@prisma/client/runtime/index-browser"
|
||||
|
||||
export type * from '../models.js'
|
||||
export type * from './prismaNamespace.js'
|
||||
|
||||
export const Decimal = runtime.Decimal
|
||||
|
||||
|
||||
export const NullTypes = {
|
||||
DbNull: runtime.objectEnumValues.classes.DbNull as (new (secret: never) => typeof runtime.objectEnumValues.instances.DbNull),
|
||||
JsonNull: runtime.objectEnumValues.classes.JsonNull as (new (secret: never) => typeof runtime.objectEnumValues.instances.JsonNull),
|
||||
AnyNull: runtime.objectEnumValues.classes.AnyNull as (new (secret: never) => typeof runtime.objectEnumValues.instances.AnyNull),
|
||||
}
|
||||
/**
|
||||
* Helper for filtering JSON entries that have `null` on the database (empty on the db)
|
||||
*
|
||||
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
|
||||
*/
|
||||
export const DbNull = runtime.objectEnumValues.instances.DbNull
|
||||
/**
|
||||
* Helper for filtering JSON entries that have JSON `null` values (not empty on the db)
|
||||
*
|
||||
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
|
||||
*/
|
||||
export const JsonNull = runtime.objectEnumValues.instances.JsonNull
|
||||
/**
|
||||
* Helper for filtering JSON entries that are `Prisma.DbNull` or `Prisma.JsonNull`
|
||||
*
|
||||
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
|
||||
*/
|
||||
export const AnyNull = runtime.objectEnumValues.instances.AnyNull
|
||||
|
||||
|
||||
export const ModelName = {
|
||||
users: 'users',
|
||||
activity_logs: 'activity_logs',
|
||||
training_menus: 'training_menus',
|
||||
user_recaps: 'user_recaps'
|
||||
} as const
|
||||
|
||||
export type ModelName = (typeof ModelName)[keyof typeof ModelName]
|
||||
|
||||
/*
|
||||
* Enums
|
||||
*/
|
||||
|
||||
export const TransactionIsolationLevel = runtime.makeStrictEnum({
|
||||
ReadUncommitted: 'ReadUncommitted',
|
||||
ReadCommitted: 'ReadCommitted',
|
||||
RepeatableRead: 'RepeatableRead',
|
||||
Serializable: 'Serializable'
|
||||
} as const)
|
||||
|
||||
export type TransactionIsolationLevel = (typeof TransactionIsolationLevel)[keyof typeof TransactionIsolationLevel]
|
||||
|
||||
|
||||
export const UsersScalarFieldEnum = {
|
||||
id: 'id',
|
||||
name: 'name',
|
||||
role: 'role',
|
||||
coach_id: 'coach_id',
|
||||
created_at: 'created_at'
|
||||
} as const
|
||||
|
||||
export type UsersScalarFieldEnum = (typeof UsersScalarFieldEnum)[keyof typeof UsersScalarFieldEnum]
|
||||
|
||||
|
||||
export const Activity_logsScalarFieldEnum = {
|
||||
id: 'id',
|
||||
timestamp: 'timestamp',
|
||||
status: 'status',
|
||||
confidence: 'confidence',
|
||||
details: 'details',
|
||||
user_id: 'user_id'
|
||||
} as const
|
||||
|
||||
export type Activity_logsScalarFieldEnum = (typeof Activity_logsScalarFieldEnum)[keyof typeof Activity_logsScalarFieldEnum]
|
||||
|
||||
|
||||
export const Training_menusScalarFieldEnum = {
|
||||
id: 'id',
|
||||
name: 'name',
|
||||
exercises: 'exercises',
|
||||
created_at: 'created_at',
|
||||
author_id: 'author_id',
|
||||
client_id: 'client_id'
|
||||
} as const
|
||||
|
||||
export type Training_menusScalarFieldEnum = (typeof Training_menusScalarFieldEnum)[keyof typeof Training_menusScalarFieldEnum]
|
||||
|
||||
|
||||
export const User_recapsScalarFieldEnum = {
|
||||
id: 'id',
|
||||
menu_id: 'menu_id',
|
||||
user_id: 'user_id',
|
||||
summary: 'summary',
|
||||
completed_at: 'completed_at'
|
||||
} as const
|
||||
|
||||
export type User_recapsScalarFieldEnum = (typeof User_recapsScalarFieldEnum)[keyof typeof User_recapsScalarFieldEnum]
|
||||
|
||||
|
||||
export const SortOrder = {
|
||||
asc: 'asc',
|
||||
desc: 'desc'
|
||||
} as const
|
||||
|
||||
export type SortOrder = (typeof SortOrder)[keyof typeof SortOrder]
|
||||
|
||||
|
||||
export const NullableJsonNullValueInput = {
|
||||
DbNull: DbNull,
|
||||
JsonNull: JsonNull
|
||||
} as const
|
||||
|
||||
export type NullableJsonNullValueInput = (typeof NullableJsonNullValueInput)[keyof typeof NullableJsonNullValueInput]
|
||||
|
||||
|
||||
export const QueryMode = {
|
||||
default: 'default',
|
||||
insensitive: 'insensitive'
|
||||
} as const
|
||||
|
||||
export type QueryMode = (typeof QueryMode)[keyof typeof QueryMode]
|
||||
|
||||
|
||||
export const NullsOrder = {
|
||||
first: 'first',
|
||||
last: 'last'
|
||||
} as const
|
||||
|
||||
export type NullsOrder = (typeof NullsOrder)[keyof typeof NullsOrder]
|
||||
|
||||
|
||||
export const JsonNullValueFilter = {
|
||||
DbNull: DbNull,
|
||||
JsonNull: JsonNull,
|
||||
AnyNull: AnyNull
|
||||
} as const
|
||||
|
||||
export type JsonNullValueFilter = (typeof JsonNullValueFilter)[keyof typeof JsonNullValueFilter]
|
||||
|
||||
15
app/generated/client/models.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
// @ts-nocheck
|
||||
/*
|
||||
* This is a barrel export file for all models and their related types.
|
||||
*
|
||||
* 🟢 You can import this file directly.
|
||||
*/
|
||||
export type * from './models/users.js'
|
||||
export type * from './models/activity_logs.js'
|
||||
export type * from './models/training_menus.js'
|
||||
export type * from './models/user_recaps.js'
|
||||
export type * from './commonInputTypes.js'
|
||||
1435
app/generated/client/models/activity_logs.ts
Normal file
1715
app/generated/client/models/training_menus.ts
Normal file
1543
app/generated/client/models/user_recaps.ts
Normal file
2038
app/generated/client/models/users.ts
Normal file
BIN
app/generated/client/query_engine-windows.dll.node
Normal file
27
app/globals.css
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
@import "tailwindcss";
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
/* Friendly Palette */
|
||||
--color-primary: #5682B1; /* Steel Blue */
|
||||
--color-secondary: #739EC9; /* Soft Blue */
|
||||
--color-highlight: #404040; /* Dark Grey for text contrast, replacing peach which is too light for text */
|
||||
--color-accent: #FFE8DB; /* Peach (New variable for backgrounds) */
|
||||
}
|
||||
|
||||
/* Friendly Light Theme Default */
|
||||
:root {
|
||||
--background: #FFFFFF;
|
||||
--foreground: #171717;
|
||||
}
|
||||
|
||||
/* Dark mode removed to force Friendly Light Theme */
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
37
app/layout.tsx
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "STRAPS",
|
||||
description: "Smart Training & Rehab System",
|
||||
icons: {
|
||||
icon: [{ url: "/favicon.svg", type: "image/svg+xml" }],
|
||||
},
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
310
app/page.tsx
Normal file
|
|
@ -0,0 +1,310 @@
|
|||
|
||||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { motion } from 'framer-motion';
|
||||
import { User, Shield, ArrowRight, Activity } from 'lucide-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { AuthProvider, useAuth } from '@/lib/auth';
|
||||
|
||||
export default function LandingPageWrap() {
|
||||
return (
|
||||
<AuthProvider>
|
||||
<LandingPage />
|
||||
</AuthProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function LandingPage() {
|
||||
const router = useRouter();
|
||||
const { login } = useAuth();
|
||||
const [role, setRole] = useState<'coach' | 'client' | null>(null);
|
||||
const [userId, setUserId] = useState('');
|
||||
const [status, setStatus] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
// Registration State
|
||||
const [isRegistering, setIsRegistering] = useState(false);
|
||||
const [regName, setRegName] = useState('');
|
||||
const [regRole, setRegRole] = useState<'COACH' | 'CLIENT' | null>(null);
|
||||
const [generatedUser, setGeneratedUser] = useState<any>(null);
|
||||
|
||||
const handleRegister = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!regName || !regRole) return;
|
||||
|
||||
setIsLoading(true);
|
||||
setStatus('Creating Account...');
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/register', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name: regName, role: regRole })
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
const newUser = await res.json();
|
||||
setGeneratedUser(newUser);
|
||||
setStatus('Account Created!');
|
||||
} else {
|
||||
setStatus('Registration Failed');
|
||||
}
|
||||
} catch (err) {
|
||||
setStatus('Error connecting to server');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogin = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!role || !userId) return;
|
||||
|
||||
setIsLoading(true);
|
||||
setStatus('Validating credentials...');
|
||||
|
||||
try {
|
||||
const user = await login(userId);
|
||||
if (user) {
|
||||
// Role Validation
|
||||
if (role === 'coach') {
|
||||
if (user.role !== 'COACH') {
|
||||
setStatus('Access Denied: You are not a Coach');
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
setStatus('Authenticated. Redirecting...');
|
||||
router.push('/coach/dashboard');
|
||||
} else {
|
||||
if (user.role !== 'CLIENT') {
|
||||
setStatus('Access Denied: You are not a Client');
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
setStatus('Authenticated. Redirecting...');
|
||||
router.push('/client');
|
||||
}
|
||||
} else {
|
||||
setStatus('Invalid User ID');
|
||||
setIsLoading(false);
|
||||
}
|
||||
} catch (err) {
|
||||
setStatus('Connection Error');
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-white text-zinc-900 font-sans selection:bg-blue-100 flex flex-col">
|
||||
{/* Decorative Background Elements */}
|
||||
<div className="fixed inset-0 overflow-hidden pointer-events-none">
|
||||
<div className="absolute top-0 left-0 w-[500px] h-[500px] bg-blue-50 rounded-full blur-3xl -translate-x-1/2 -translate-y-1/2 opacity-70"></div>
|
||||
<div className="absolute bottom-0 right-0 w-[500px] h-[500px] bg-indigo-50 rounded-full blur-3xl translate-x-1/2 translate-y-1/2 opacity-70"></div>
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 flex-1 flex flex-col items-center justify-center p-6 md:p-12">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8 }}
|
||||
className="max-w-md w-full"
|
||||
>
|
||||
{/* Logo / Header */}
|
||||
<div className="text-center mb-10">
|
||||
<h1 className="text-5xl font-light tracking-tight text-zinc-900 mb-2">
|
||||
STRAPS<span className="font-bold text-primary">-R</span>
|
||||
</h1>
|
||||
<p className="text-zinc-500 text-lg tracking-wide">Strength Training Pose Recognition and Patient Rehabilitation</p>
|
||||
</div>
|
||||
|
||||
{/* Login / Register Card */}
|
||||
<div className="bg-white/80 backdrop-blur-xl border border-white/50 shadow-xl rounded-3xl p-8 md:p-10 relative overflow-hidden transition-all">
|
||||
|
||||
{/* Mode Toggle */}
|
||||
<div className="flex justify-center mb-8">
|
||||
<div className="bg-zinc-100 p-1 rounded-full flex">
|
||||
<button
|
||||
onClick={() => { setIsRegistering(false); setGeneratedUser(null); setStatus(''); }}
|
||||
className={`px-6 py-2 rounded-full text-sm font-bold transition-all ${!isRegistering ? 'bg-white shadow text-zinc-900' : 'text-zinc-500'}`}
|
||||
>
|
||||
LOGIN
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { setIsRegistering(true); setGeneratedUser(null); setStatus(''); }}
|
||||
className={`px-6 py-2 rounded-full text-sm font-bold transition-all ${isRegistering ? 'bg-white shadow text-zinc-900' : 'text-zinc-500'}`}
|
||||
>
|
||||
REGISTER
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isRegistering ? (
|
||||
generatedUser ? (
|
||||
<div className="text-center space-y-6 animate-in fade-in slide-in-from-bottom-4">
|
||||
<div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mx-auto text-green-600">
|
||||
<Shield className="w-8 h-8" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xl font-bold text-zinc-900">Welcome, {generatedUser.name}!</h3>
|
||||
<p className="text-zinc-500 text-sm mt-1">Your account has been created.</p>
|
||||
</div>
|
||||
<div className="bg-zinc-50 border-2 border-dashed border-zinc-200 p-6 rounded-2xl">
|
||||
<p className="text-xs font-bold text-zinc-400 uppercase tracking-widest mb-2">Your Unique User ID</p>
|
||||
<p className="text-4xl font-black text-primary tracking-tighter">{generatedUser.id}</p>
|
||||
</div>
|
||||
<p className="text-xs text-red-400">Please save this ID. You will need it to login.</p>
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsRegistering(false);
|
||||
setRole(generatedUser.role === 'COACH' ? 'coach' : 'client');
|
||||
setUserId(generatedUser.id.toString());
|
||||
setGeneratedUser(null);
|
||||
}}
|
||||
className="w-full py-4 bg-zinc-900 text-white rounded-xl font-bold tracking-widest uppercase hover:bg-black transition-all"
|
||||
>
|
||||
Go to Login
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={handleRegister} className="space-y-6 animate-in fade-in slide-in-from-bottom-4">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="text-xs font-bold text-zinc-400 uppercase tracking-widest ml-1">Full Name</label>
|
||||
<input
|
||||
required
|
||||
type="text"
|
||||
placeholder="e.g. John Doe"
|
||||
value={regName}
|
||||
onChange={(e) => setRegName(e.target.value)}
|
||||
className="w-full bg-zinc-50 border border-zinc-200 rounded-xl px-4 py-3 text-lg focus:outline-none focus:border-zinc-400 transition-colors text-zinc-900"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs font-bold text-zinc-400 uppercase tracking-widest ml-1">Role</label>
|
||||
<div className="grid grid-cols-2 gap-3 mt-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setRegRole('COACH')}
|
||||
className={`p-3 rounded-xl border-2 transition-all flex flex-col items-center gap-2 ${
|
||||
regRole === 'COACH'
|
||||
? 'border-primary bg-blue-50 text-primary'
|
||||
: 'border-zinc-100 bg-zinc-50 text-zinc-400'
|
||||
}`}
|
||||
>
|
||||
<Shield className="w-5 h-5" />
|
||||
<span className="font-bold text-xs">COACH</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setRegRole('CLIENT')}
|
||||
className={`p-3 rounded-xl border-2 transition-all flex flex-col items-center gap-2 ${
|
||||
regRole === 'CLIENT'
|
||||
? 'border-emerald-500 bg-emerald-50 text-emerald-600'
|
||||
: 'border-zinc-100 bg-zinc-50 text-zinc-400'
|
||||
}`}
|
||||
>
|
||||
<User className="w-5 h-5" />
|
||||
<span className="font-bold text-xs">CLIENT</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!regName || !regRole || isLoading}
|
||||
className={`w-full py-4 rounded-xl font-bold tracking-widest uppercase transition-all flex items-center justify-center gap-2 ${
|
||||
regName && regRole && !isLoading
|
||||
? 'bg-zinc-900 text-white hover:bg-black shadow-lg'
|
||||
: 'bg-zinc-100 text-zinc-300 cursor-not-allowed'
|
||||
}`}
|
||||
>
|
||||
{isLoading ? 'Creating...' : 'Create Account'}
|
||||
</button>
|
||||
</form>
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
{/* Role Selection */}
|
||||
<div className="grid grid-cols-2 gap-4 mb-8">
|
||||
<button
|
||||
onClick={() => setRole('coach')}
|
||||
className={`p-4 rounded-xl border-2 transition-all flex flex-col items-center gap-3 ${
|
||||
role === 'coach'
|
||||
? 'border-primary bg-blue-50/50 text-primary shadow-sm'
|
||||
: 'border-zinc-100 bg-zinc-50 text-zinc-400 hover:border-zinc-200 hover:bg-zinc-100'
|
||||
}`}
|
||||
>
|
||||
<Shield className={`w-8 h-8 ${role === 'coach' ? 'fill-current' : ''}`} />
|
||||
<span className="font-bold tracking-wider text-xs uppercase">Coach</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setRole('client')}
|
||||
className={`p-4 rounded-xl border-2 transition-all flex flex-col items-center gap-3 ${
|
||||
role === 'client'
|
||||
? 'border-emerald-500 bg-emerald-50/50 text-emerald-600 shadow-sm'
|
||||
: 'border-zinc-100 bg-zinc-50 text-zinc-400 hover:border-zinc-200 hover:bg-zinc-100'
|
||||
}`}
|
||||
>
|
||||
<User className={`w-8 h-8 ${role === 'client' ? 'fill-current' : ''}`} />
|
||||
<span className="font-bold tracking-wider text-xs uppercase">Client</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Login Form */}
|
||||
<form onSubmit={handleLogin} className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-bold text-zinc-400 uppercase tracking-widest ml-1">
|
||||
{role === 'coach' ? 'Coach Identifier' : 'Client Identifier'}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
disabled={!role || isLoading}
|
||||
placeholder={!role ? "Select a role above" : "Enter ID (e.g. A8#k9P)"}
|
||||
value={userId}
|
||||
onChange={(e) => setUserId(e.target.value)}
|
||||
className="w-full bg-zinc-50 border border-zinc-200 rounded-xl px-4 py-4 text-lg text-center tracking-widest focus:outline-none focus:border-zinc-400 transition-colors disabled:opacity-50 disabled:cursor-not-allowed text-zinc-900 placeholder:text-zinc-300 font-mono"
|
||||
/>
|
||||
{status && <p className="text-center text-xs font-bold text-primary animate-pulse">{status}</p>}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!role || isLoading}
|
||||
className={`w-full py-4 rounded-xl font-bold tracking-widest uppercase transition-all flex items-center justify-center gap-2 ${
|
||||
role
|
||||
? 'bg-zinc-900 text-white hover:bg-black shadow-lg hover:shadow-xl hover:-translate-y-0.5'
|
||||
: 'bg-zinc-100 text-zinc-300 cursor-not-allowed'
|
||||
}`}
|
||||
>
|
||||
{isLoading ? (
|
||||
<span className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
Enter Platform <ArrowRight className="w-4 h-4" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Active Status Indicator */}
|
||||
<div className="absolute top-6 right-6 flex items-center gap-2">
|
||||
<span className="relative flex h-2 w-2">
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span>
|
||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-green-500"></span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-center text-zinc-400 text-xs mt-8 tracking-widest">
|
||||
SECURE ACCESS • v2.0.4
|
||||
</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
26
debug_menu.js
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
|
||||
const { PrismaClient } = require('./app/generated/client');
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
const menus = await prisma.training_menus.findMany({
|
||||
orderBy: { id: 'desc' },
|
||||
take: 1
|
||||
});
|
||||
|
||||
if (menus.length > 0) {
|
||||
console.log("Latest Menu:", JSON.stringify(menus[0], null, 2));
|
||||
const ex = menus[0].exercises;
|
||||
if (typeof ex === 'string') {
|
||||
console.log("Exercises (parsed):", JSON.stringify(JSON.parse(ex), null, 2));
|
||||
} else {
|
||||
console.log("Exercises (raw):", JSON.stringify(ex, null, 2));
|
||||
}
|
||||
} else {
|
||||
console.log("No menus found.");
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
.catch(e => console.error(e))
|
||||
.finally(async () => await prisma.$disconnect());
|
||||
18
eslint.config.mjs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { defineConfig, globalIgnores } from "eslint/config";
|
||||
import nextVitals from "eslint-config-next/core-web-vitals";
|
||||
import nextTs from "eslint-config-next/typescript";
|
||||
|
||||
const eslintConfig = defineConfig([
|
||||
...nextVitals,
|
||||
...nextTs,
|
||||
// Override default ignores of eslint-config-next.
|
||||
globalIgnores([
|
||||
// Default ignores of eslint-config-next:
|
||||
".next/**",
|
||||
"out/**",
|
||||
"build/**",
|
||||
"next-env.d.ts",
|
||||
]),
|
||||
]);
|
||||
|
||||
export default eslintConfig;
|
||||
BIN
exercise_rules.pdf
Normal file
3380
integrated.js
Normal file
75
lib/auth.tsx
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
|
||||
import React, { createContext, useContext, useEffect, useState } from 'react';
|
||||
|
||||
type UserRole = 'COACH' | 'CLIENT';
|
||||
|
||||
interface User {
|
||||
id: string; // Changed to string
|
||||
name: string;
|
||||
role: UserRole;
|
||||
coach_id?: string | null;
|
||||
coach?: {
|
||||
id: string;
|
||||
name: string;
|
||||
} | null;
|
||||
}
|
||||
|
||||
interface AuthContextType {
|
||||
user: User | null;
|
||||
login: (userId: string) => Promise<User | null>; // Changed to string
|
||||
logout: () => void;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextType>({
|
||||
user: null,
|
||||
login: async () => null,
|
||||
logout: () => {},
|
||||
isLoading: true,
|
||||
});
|
||||
|
||||
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
// Check sessionStorage on mount
|
||||
const storedUser = sessionStorage.getItem('straps_user');
|
||||
if (storedUser) {
|
||||
setUser(JSON.parse(storedUser));
|
||||
}
|
||||
setIsLoading(false);
|
||||
}, []);
|
||||
|
||||
const login = async (id: string): Promise<User | null> => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const res = await fetch(`/api/users/${id}`);
|
||||
if (res.ok) {
|
||||
const userData = await res.json();
|
||||
setUser(userData);
|
||||
sessionStorage.setItem('straps_user', JSON.stringify(userData)); // Store full object in Session
|
||||
return userData;
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error("Login failed", error);
|
||||
return null;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
setUser(null);
|
||||
sessionStorage.removeItem('straps_user');
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{ user, login, logout, isLoading }}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export const useAuth = () => useContext(AuthContext);
|
||||
7
lib/mediapipe-shim.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
|
||||
// This shim bridges the gap between Webpack and the MediaPipe global script
|
||||
module.exports = {
|
||||
get Pose() { return (typeof window !== 'undefined' ? window.Pose : undefined); },
|
||||
get POSE_CONNECTIONS() { return (typeof window !== 'undefined' ? window.POSE_CONNECTIONS : undefined); },
|
||||
get VERSION() { return (typeof window !== 'undefined' ? window.VERSION : undefined); }
|
||||
};
|
||||
141
lib/pose/ExerciseRules.ts
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
|
||||
export interface Landmark {
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
visibility?: number;
|
||||
}
|
||||
|
||||
export interface AnglesDict {
|
||||
[key: string]: number;
|
||||
}
|
||||
|
||||
export type FormValidationResult = {
|
||||
valid: boolean;
|
||||
feedback: string[];
|
||||
};
|
||||
|
||||
// Expanded Configuration Interface to match Python
|
||||
export interface ExerciseConfig {
|
||||
name: string;
|
||||
// Core Identification
|
||||
detection: {
|
||||
shoulder_static?: [number, number];
|
||||
shoulder_down?: [number, number];
|
||||
hip_static?: [number, number];
|
||||
};
|
||||
// Counting Logic
|
||||
phase_type: 'start_down' | 'start_up';
|
||||
dynamic_angles: {
|
||||
[key: string]: [number, number]; // e.g., elbow_up: [0, 60]
|
||||
};
|
||||
// Scoring & Validation
|
||||
static_angles?: { [key: string]: number }; // Ideal static angle
|
||||
wrist_distance?: [number, number];
|
||||
convex_hull?: {
|
||||
up?: [number, number];
|
||||
down?: [number, number];
|
||||
};
|
||||
|
||||
// Legacy support (optional)
|
||||
form_rules?: Array<(landmarks: Landmark[], angles: AnglesDict, side?: 'left' | 'right') => FormValidationResult>;
|
||||
}
|
||||
|
||||
export const EXERCISE_CONFIGS: { [key: string]: ExerciseConfig } = {
|
||||
'bicep_curl': {
|
||||
name: "Bicep Curl",
|
||||
phase_type: 'start_down',
|
||||
detection: { shoulder_static: [0, 30] },
|
||||
dynamic_angles: {
|
||||
'elbow_down': [140, 180],
|
||||
'elbow_up': [0, 85],
|
||||
'shoulder_down': [0, 30],
|
||||
'shoulder_up': [0, 60]
|
||||
},
|
||||
static_angles: { 'shoulder_r': 15, 'shoulder_l': 15 },
|
||||
wrist_distance: [0, 0.3],
|
||||
convex_hull: { down: [0, 0.05], up: [0.05, 0.2] }
|
||||
},
|
||||
'hammer_curl': {
|
||||
name: "Hammer Curl",
|
||||
phase_type: 'start_down',
|
||||
detection: { shoulder_static: [0, 30] },
|
||||
dynamic_angles: {
|
||||
'elbow_down': [120, 180],
|
||||
'elbow_up': [0, 85], // Similar to bicep, maybe slightly different in 3D but same in 2D
|
||||
'shoulder_down': [0, 30],
|
||||
'shoulder_up': [0, 60]
|
||||
},
|
||||
static_angles: { 'shoulder_r': 15, 'shoulder_l': 15 },
|
||||
wrist_distance: [0, 0.2], // Hammer curl usually keeps weights closer?
|
||||
convex_hull: { down: [0, 0.05], up: [0.05, 0.2] }
|
||||
},
|
||||
'shoulder_press': { // Overhead Press
|
||||
name: "Overhead Press",
|
||||
phase_type: 'start_down', // Starts at shoulders, goes UP. Actually "Down" state is hands at shoulders. "Up" is hands in air.
|
||||
detection: { shoulder_down: [50, 120] }, // Relaxed detection
|
||||
dynamic_angles: {
|
||||
'elbow_down': [20, 100], // Relaxed bottom position (can stop at chin level)
|
||||
'elbow_up': [150, 180], // Relaxed lockout (sometimes 140 is enough)
|
||||
'shoulder_down': [40, 110], // Relaxed shoulder range
|
||||
'shoulder_up': [130, 180] // Relaxed top range
|
||||
},
|
||||
static_angles: { 'hip_r': 165, 'hip_l': 165 }, // Standing straight
|
||||
wrist_distance: [0, 0.3],
|
||||
convex_hull: { down: [0.05, 0.15], up: [0.15, 0.3] }
|
||||
},
|
||||
'lateral_raises': {
|
||||
name: "Lateral Raises",
|
||||
phase_type: 'start_down', // Arms at sides
|
||||
detection: {},
|
||||
dynamic_angles: {
|
||||
'shoulder_down': [0, 30],
|
||||
'shoulder_up': [80, 110], // T-pose
|
||||
'elbow_down': [140, 180], // Straight arm
|
||||
'elbow_up': [140, 180] // Keep arms straight
|
||||
},
|
||||
static_angles: { 'elbow_r': 160, 'elbow_l': 160 },
|
||||
convex_hull: { down: [0, 0.1], up: [0.2, 0.4] } // Wide hull when arms up
|
||||
},
|
||||
'squat': {
|
||||
name: "Squat",
|
||||
phase_type: 'start_up', // Standing -> Squat -> Standing
|
||||
detection: {},
|
||||
dynamic_angles: {
|
||||
'hip_up': [160, 180], // Standing
|
||||
'hip_down': [50, 100], // Squat depth
|
||||
'knee_up': [160, 180],
|
||||
'knee_down': [50, 100]
|
||||
},
|
||||
static_angles: { 'shoulder_r': 20, 'shoulder_l': 20 }, // Torso relatively upright
|
||||
convex_hull: { up: [0.1, 0.2], down: [0.05, 0.15] } // Hull shrinks when squatting? Or stays same?
|
||||
},
|
||||
'deadlift': {
|
||||
name: "Deadlift",
|
||||
// Actually deadlift starts on floor. So 'start_down' (hips low) -> 'up' (hips high/standing).
|
||||
phase_type: 'start_down', // Down (at floor) -> Up (Standing). Warning: Logic usually assumes "Down" means "Rest/Start".
|
||||
detection: {},
|
||||
dynamic_angles: {
|
||||
'hip_down': [45, 100], // Hips flexed at bottom
|
||||
'hip_up': [160, 180], // Hips extended at top
|
||||
'knee_down': [60, 120], // Knees bent
|
||||
'knee_up': [160, 180] // Knees locked
|
||||
},
|
||||
static_angles: { 'elbow_r': 170, 'elbow_l': 170 }, // Arms straight
|
||||
convex_hull: { down: [0.1, 0.2], up: [0.1, 0.2] }
|
||||
},
|
||||
'lunges': {
|
||||
name: "Lunges",
|
||||
phase_type: 'start_up', // Standing -> Lunge -> Standing
|
||||
detection: {},
|
||||
dynamic_angles: {
|
||||
'knee_up': [160, 180], // Standing
|
||||
'knee_down': [70, 110], // Lunge depth
|
||||
'hip_up': [160, 180],
|
||||
'hip_down': [70, 110]
|
||||
},
|
||||
static_angles: {},
|
||||
convex_hull: {}
|
||||
}
|
||||
};
|
||||
|
||||
151
lib/pose/HARCore.ts
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
|
||||
import { XGBoostPredictor } from './XGBoostPredictor';
|
||||
import { Landmark, EXERCISE_CONFIGS } from './ExerciseRules';
|
||||
import { RehabCore } from './RehabCore';
|
||||
import { calculateAngle } from './MathUtils';
|
||||
|
||||
// Label Encoder mapping from python: Classes: ['berdiri' 'duduk' 'jatuh']
|
||||
const LABELS = ['Standing', 'Sitting', 'Fall Detected'];
|
||||
|
||||
export class HARCore {
|
||||
private predictor: XGBoostPredictor;
|
||||
private rehab: RehabCore;
|
||||
private currentExercise: string | null = null;
|
||||
|
||||
constructor() {
|
||||
this.predictor = new XGBoostPredictor();
|
||||
this.rehab = new RehabCore();
|
||||
}
|
||||
|
||||
// Set the active exercise to track (from menu)
|
||||
public setExercise(name: string) {
|
||||
// Map UI name to internal config name if needed
|
||||
// For now assume direct match e.g. "Bicep Curl" -> "bicep_curls"
|
||||
// Simple normalizer
|
||||
const lowerName = name.toLowerCase();
|
||||
|
||||
// Find matching key in EXERCISE_CONFIGS
|
||||
// EXERCISE_CONFIGS keys are like 'bicep_curl', 'squat'
|
||||
// UI names might be "Bicep Curl", "Squats"
|
||||
const key = Object.keys(EXERCISE_CONFIGS).find(k =>
|
||||
lowerName.includes(k.replace('_', ' ')) ||
|
||||
k.replace('_', ' ').includes(lowerName.split(' ')[0])
|
||||
);
|
||||
|
||||
this.currentExercise = key || null;
|
||||
}
|
||||
|
||||
public resetParams() {
|
||||
this.rehab.reset();
|
||||
// this.currentExercise = null; // Don't nullify exercise, just counters
|
||||
}
|
||||
|
||||
public async process(landmarks: Landmark[], worldLandmarks: Landmark[] = []) {
|
||||
if (!landmarks || landmarks.length === 0) return null;
|
||||
|
||||
// 1. Activity Recognition (HAR) - XGBoost
|
||||
const features = this.extractFeatures(landmarks);
|
||||
const probs = this.predictor.predict(features);
|
||||
const maxIdx = probs.indexOf(Math.max(...probs));
|
||||
const status = LABELS[maxIdx];
|
||||
const confidence = probs[maxIdx];
|
||||
|
||||
// 2. Exercise Counting (Rehab) - Heuristic
|
||||
let reps = 0;
|
||||
let feedback = "";
|
||||
let debug = {};
|
||||
|
||||
if (this.currentExercise) {
|
||||
const result = this.rehab.process(this.currentExercise, landmarks, worldLandmarks);
|
||||
if (result) {
|
||||
// Combine left/right reps for total or max?
|
||||
// Usually we want total completed reps.
|
||||
reps = this.rehab.getReps(this.currentExercise);
|
||||
|
||||
// Construct feedback
|
||||
const stateL = result.left.stage;
|
||||
const stateR = result.right.stage;
|
||||
feedback = `L: ${stateL || '-'} | R: ${stateR || '-'}`;
|
||||
if (result.feedback && result.feedback.length > 0) {
|
||||
feedback += ` | ${result.feedback}`; // Add generic feedback
|
||||
}
|
||||
|
||||
debug = {
|
||||
angles: { l: result.left.angle, r: result.right.angle },
|
||||
scores: result.scores
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
status,
|
||||
confidence,
|
||||
exercise: this.currentExercise,
|
||||
reps,
|
||||
feedback,
|
||||
debug
|
||||
};
|
||||
}
|
||||
|
||||
private extractFeatures(landmarks: Landmark[]): number[] {
|
||||
// 1. Flatten Raw Keypoints (33 * 4 = 132 features)
|
||||
const raw: number[] = [];
|
||||
landmarks.forEach(lm => {
|
||||
raw.push(lm.x, lm.y, lm.z, lm.visibility || 0);
|
||||
});
|
||||
|
||||
// 2. Derived Features
|
||||
// Helper to get landmark
|
||||
const getLm = (idx: number) => landmarks[idx];
|
||||
|
||||
// Helper to flatten {x,y} to point for calculateAngle
|
||||
const pt = (lm: Landmark) => ({x: lm.x, y: lm.y});
|
||||
const calcAng = (a: Landmark, b: Landmark, c: Landmark) => calculateAngle(pt(a), pt(b), pt(c));
|
||||
|
||||
const derived: number[] = [];
|
||||
|
||||
// Angles
|
||||
// 0: Left Elbow (11-13-15)
|
||||
derived.push(calcAng(getLm(11), getLm(13), getLm(15)));
|
||||
// 1: Right Elbow (12-14-16)
|
||||
derived.push(calcAng(getLm(12), getLm(14), getLm(16)));
|
||||
// 2: Left Hip (11-23-25)
|
||||
derived.push(calcAng(getLm(11), getLm(23), getLm(25)));
|
||||
// 3: Right Hip (12-24-26)
|
||||
derived.push(calcAng(getLm(12), getLm(24), getLm(26)));
|
||||
// 4: Left Knee (23-25-27)
|
||||
derived.push(calcAng(getLm(23), getLm(25), getLm(27)));
|
||||
// 5: Right Knee (24-26-28)
|
||||
derived.push(calcAng(getLm(24), getLm(26), getLm(28)));
|
||||
|
||||
// Distances & Ratios
|
||||
const dist = (a: Landmark, b: Landmark) => Math.sqrt(Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2));
|
||||
|
||||
const shoulderWidth = dist(getLm(11), getLm(12));
|
||||
const hipWidth = dist(getLm(23), getLm(24));
|
||||
|
||||
const midShoulder = { x: (getLm(11).x + getLm(12).x)/2, y: (getLm(11).y + getLm(12).y)/2 };
|
||||
const midHip = { x: (getLm(23).x + getLm(24).x)/2, y: (getLm(23).y + getLm(24).y)/2 };
|
||||
const torsoHeight = dist(midShoulder as Landmark, midHip as Landmark);
|
||||
|
||||
const eps = 1e-6;
|
||||
// 6: Shoulder Width Ratio
|
||||
derived.push(shoulderWidth / (torsoHeight + eps));
|
||||
// 7: Hip Width Ratio
|
||||
derived.push(hipWidth / (torsoHeight + eps));
|
||||
|
||||
// 8: Torso Vertical Alignment (Cosine Similarity with [0, -1])
|
||||
// Vector from Hip to Shoulder (Upwards)
|
||||
const torsoVec = { x: midShoulder.x - midHip.x, y: midShoulder.y - midHip.y };
|
||||
const verticalVec = { x: 0, y: -1 }; // Up in image coordinates (y is down)?
|
||||
// Python: vertical_vector = np.array([0, -1])
|
||||
// In python/opencv y is down. So [0, -1] is UP vector.
|
||||
|
||||
const dot = (torsoVec.x * verticalVec.x) + (torsoVec.y * verticalVec.y);
|
||||
const norm = Math.sqrt(torsoVec.x * torsoVec.x + torsoVec.y * torsoVec.y);
|
||||
|
||||
derived.push(norm > 0 ? dot / norm : 0);
|
||||
|
||||
return [...raw, ...derived];
|
||||
}
|
||||
}
|
||||
176
lib/pose/MathUtils.ts
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
|
||||
import { Landmark } from './ExerciseRules';
|
||||
|
||||
// --- Types ---
|
||||
export type Point = { x: number; y: number };
|
||||
|
||||
// --- Basic Geometry ---
|
||||
|
||||
export function computeDistance(p1: Point, p2: Point): number {
|
||||
return Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2));
|
||||
}
|
||||
|
||||
export function calculateAngle(a: Point, b: Point, c: Point): number {
|
||||
const radians = Math.atan2(c.y - b.y, c.x - b.x) - Math.atan2(a.y - b.y, a.x - b.x);
|
||||
let angle = Math.abs((radians * 180.0) / Math.PI);
|
||||
if (angle > 180.0) angle = 360 - angle;
|
||||
return angle;
|
||||
}
|
||||
|
||||
export function inRange(val: number, low: number, high: number): boolean {
|
||||
return val >= low && val <= high;
|
||||
}
|
||||
|
||||
// --- Normalization (Port of normalize_v2) ---
|
||||
|
||||
function formatLandmark(lm: Landmark): Point {
|
||||
return { x: lm.x, y: lm.y };
|
||||
}
|
||||
|
||||
export function normalizeLandmarks(landmarks: Landmark[]): Point[] {
|
||||
// Indices for torso: 11(sho_l), 12(sho_r), 23(hip_l), 24(hip_r)
|
||||
// Note: Python mediapipe indices match JS.
|
||||
const indices = [11, 12, 23, 24];
|
||||
|
||||
// Prepare points for least squares (Torso alignment)
|
||||
const pts = indices.map(i => ({ x: landmarks[i].x, y: landmarks[i].y }));
|
||||
|
||||
// Linear Regression (Least Squares) to find torso centerline angle
|
||||
// We want line y = mx + c. But vertical lines fail, so we often do PCA or simple regression.
|
||||
// Python code uses np.linalg.lstsq on X to predict Y.
|
||||
|
||||
let sumX = 0, sumY = 0, sumXY = 0, sumXX = 0;
|
||||
const n = pts.length;
|
||||
for (const p of pts) {
|
||||
sumX += p.x;
|
||||
sumY += p.y;
|
||||
sumXY += p.x * p.y;
|
||||
sumXX += p.x * p.x;
|
||||
}
|
||||
|
||||
const m = (n * sumXY - sumX * sumY) / (n * sumXX - sumX * sumX);
|
||||
// If undefined (perfect vertical), theta is 90 deg.
|
||||
const theta = !isFinite(m) ? Math.PI / 2 : Math.atan(m);
|
||||
|
||||
const cos_t = Math.cos(-theta);
|
||||
const sin_t = Math.sin(-theta);
|
||||
|
||||
// Centers
|
||||
const sho_l = landmarks[11];
|
||||
const sho_r = landmarks[12];
|
||||
const hip_l = landmarks[23];
|
||||
const hip_r = landmarks[24];
|
||||
|
||||
const shoulder_center = { x: (sho_l.x + sho_r.x) / 2, y: (sho_l.y + sho_r.y) / 2 };
|
||||
const hip_center = { x: (hip_l.x + hip_r.x) / 2, y: (hip_l.y + hip_r.y) / 2 };
|
||||
|
||||
const scale_factor = computeDistance(shoulder_center, hip_center);
|
||||
|
||||
// Normalize logic from Python:
|
||||
// 1. Shift by hip_center
|
||||
// 2. Rotate by theta
|
||||
// 3. Scale by scale_factor
|
||||
|
||||
return landmarks.map(lm => {
|
||||
const x = lm.x - hip_center.x;
|
||||
const y = lm.y - hip_center.y;
|
||||
|
||||
const x_rot = (x * cos_t - y * sin_t) / scale_factor;
|
||||
const y_rot = (x * sin_t + y * cos_t) / scale_factor;
|
||||
|
||||
return { x: x_rot, y: y_rot };
|
||||
});
|
||||
}
|
||||
|
||||
// --- Convex Hull (Monotone Chain Algorithm) ---
|
||||
|
||||
function crossProduct(o: Point, a: Point, b: Point): number {
|
||||
return (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);
|
||||
}
|
||||
|
||||
export function computeConvexHullArea(points: Point[]): number {
|
||||
const n = points.length;
|
||||
if (n <= 2) return 0;
|
||||
|
||||
// Sort points by x, then y
|
||||
const sorted = [...points].sort((a, b) => a.x === b.x ? a.y - b.y : a.x - b.x);
|
||||
|
||||
// Build lower hull
|
||||
const lower: Point[] = [];
|
||||
for (const p of sorted) {
|
||||
while (lower.length >= 2 && crossProduct(lower[lower.length - 2], lower[lower.length - 1], p) <= 0) {
|
||||
lower.pop();
|
||||
}
|
||||
lower.push(p);
|
||||
}
|
||||
|
||||
// Build upper hull
|
||||
const upper: Point[] = [];
|
||||
for (let i = n - 1; i >= 0; i--) {
|
||||
const p = sorted[i];
|
||||
while (upper.length >= 2 && crossProduct(upper[upper.length - 2], upper[upper.length - 1], p) <= 0) {
|
||||
upper.pop();
|
||||
}
|
||||
upper.push(p);
|
||||
}
|
||||
|
||||
// Concatenate (remove last point of lower and upper as they are duplicates of start/end)
|
||||
const hull = [...lower.slice(0, -1), ...upper.slice(0, -1)];
|
||||
|
||||
// Shoelace Formula for Area
|
||||
let area = 0;
|
||||
for (let i = 0; i < hull.length; i++) {
|
||||
const j = (i + 1) % hull.length;
|
||||
area += hull[i].x * hull[j].y;
|
||||
area -= hull[j].x * hull[i].y;
|
||||
}
|
||||
|
||||
return Math.abs(area) / 2;
|
||||
}
|
||||
|
||||
// --- Scoring Utilities ---
|
||||
|
||||
export function calculateContainmentScore(userRange: [number, number], refRange: [number, number]): number {
|
||||
const [user_min, user_max] = userRange;
|
||||
const [ref_min, ref_max] = refRange;
|
||||
|
||||
if (user_min === user_max) {
|
||||
return (user_min >= ref_min && user_min <= ref_max) ? 1.0 : 0.0;
|
||||
}
|
||||
|
||||
const user_length = user_max - user_min;
|
||||
if (user_length <= 0) return 1.0;
|
||||
|
||||
const intersection_min = Math.max(user_min, ref_min);
|
||||
const intersection_max = Math.min(user_max, ref_max);
|
||||
|
||||
const intersection_length = Math.max(0, intersection_max - intersection_min);
|
||||
|
||||
return intersection_length / user_length;
|
||||
}
|
||||
|
||||
/*
|
||||
* Calculates the Mean Absolute Error (MAE) for a value against a target range.
|
||||
* If the value is within range, error is 0.
|
||||
* If outside, error is distance to the nearest bound.
|
||||
*/
|
||||
export function calculateRangeDeviation(value: number, range: [number, number]): number {
|
||||
const [min, max] = range;
|
||||
// If value is smaller than min, return difference
|
||||
if (value < min) return Math.abs(min - value);
|
||||
// If value is larger than max, return difference
|
||||
if (value > max) return Math.abs(value - max);
|
||||
// Within range, perfect score (0 deviation)
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes average deviation across multiple joints.
|
||||
*/
|
||||
export function computeMAE(errors: number[]): number {
|
||||
if (errors.length === 0) return 0;
|
||||
const sum = errors.reduce((a, b) => a + b, 0);
|
||||
return sum / errors.length;
|
||||
}
|
||||
|
||||
// End of file
|
||||
348
lib/pose/RehabCore.ts
Normal file
|
|
@ -0,0 +1,348 @@
|
|||
import { Landmark, EXERCISE_CONFIGS } from './ExerciseRules';
|
||||
import {
|
||||
computeFeatures, RepFSM, Vec3, PoseFeatures,
|
||||
BicepCurlCounter, HammerCurlCounter, OverheadPressCounter,
|
||||
LateralRaiseCounter, SquatCounter, DeadliftCounter, LungeCounter
|
||||
} from './RehabFSM';
|
||||
import { calculateRangeDeviation, computeMAE } from './MathUtils';
|
||||
|
||||
const normalizeExerciseName = (input: string): string => {
|
||||
if (!input) return '';
|
||||
const clean = input.toLowerCase().trim().replace(/\s+/g, '_'); // "Overhead Press" -> "overhead_press"
|
||||
|
||||
// Map common variations to internal keys
|
||||
if (clean.includes('bicep')) return 'bicep_curl';
|
||||
if (clean.includes('hammer')) return 'hammer_curl';
|
||||
if (clean.includes('overhead') || clean.includes('shoulder_press')) return 'shoulder_press';
|
||||
if (clean.includes('lateral')) return 'lateral_raises';
|
||||
if (clean.includes('squat')) return 'squat';
|
||||
if (clean.includes('deadlift')) return 'deadlift';
|
||||
if (clean.includes('lunge')) return 'lunges';
|
||||
|
||||
return clean; // Fallback
|
||||
};
|
||||
|
||||
// Map UI names to Counter Classes
|
||||
const COUNTER_MAP: { [key: string]: () => RepFSM[] } = {
|
||||
'bicep_curl': () => [new BicepCurlCounter('left'), new BicepCurlCounter('right')],
|
||||
'hammer_curl': () => [new HammerCurlCounter('left'), new HammerCurlCounter('right')],
|
||||
'shoulder_press': () => [new OverheadPressCounter()], // Bilateral logic inside? No, it's single counter based on avg/both
|
||||
'lateral_raises': () => [new LateralRaiseCounter()],
|
||||
'squat': () => [new SquatCounter()],
|
||||
'deadlift': () => [new DeadliftCounter()],
|
||||
'lunges': () => [new LungeCounter()] // Bilateral or unified? FSM seems unified (min of both knees)
|
||||
};
|
||||
|
||||
export class RehabCore {
|
||||
private counters: { [key: string]: RepFSM[] } = {};
|
||||
private worldLandmarksCache: Vec3[] = []; // If we had world landmarks, for now we might approximate or expect them passed
|
||||
// NEW: Threshold for warning (degrees)
|
||||
private readonly DEVIATION_THRESHOLD = 15.0;
|
||||
|
||||
constructor() {
|
||||
// Initialize all counters? Or lazy load?
|
||||
// Let's lazy load or init on reset.
|
||||
}
|
||||
|
||||
public reset() {
|
||||
this.counters = {};
|
||||
console.log("RehabCore: Counters reset");
|
||||
}
|
||||
|
||||
// --- UPDATED: Comprehensive 6-Way Wrong Exercise Detection ---
|
||||
private validateExerciseType(
|
||||
configKey: string,
|
||||
features: PoseFeatures
|
||||
): string | null {
|
||||
// Feature Extraction
|
||||
const minKneeAngle = Math.min(features.leftKnee, features.rightKnee);
|
||||
const isLegsBent = minKneeAngle < 130;
|
||||
|
||||
const minElbowAngle = Math.min(features.leftElbow, features.rightElbow);
|
||||
const isElbowsBent = minElbowAngle < 110;
|
||||
const isArmsStraight = minElbowAngle > 140;
|
||||
|
||||
const isHandsOverhead = (features.leftWristY < features.noseY) || (features.rightWristY < features.noseY);
|
||||
const isHandsLow = (features.leftWristY > features.leftShoulderY) && (features.rightWristY > features.rightShoulderY);
|
||||
|
||||
const diffElbow = Math.abs(features.leftElbow - features.rightElbow);
|
||||
const isAlternating = diffElbow > 40;
|
||||
const isSimultaneous = diffElbow < 20;
|
||||
|
||||
if (configKey === 'bicep_curl') {
|
||||
if (isLegsBent) return "Detected: Squat/Lunge. Stand straight for Curls.";
|
||||
if (isHandsOverhead) return "Detected: Overhead Press. Keep elbows down.";
|
||||
if (isArmsStraight && !isHandsLow) return "Detected: Lateral Raise. Bend your elbows.";
|
||||
if (isAlternating) return "Detected: Hammer Curl (Alternating). Move both arms together.";
|
||||
return null;
|
||||
}
|
||||
|
||||
if (configKey === 'hammer_curl') {
|
||||
if (isLegsBent) return "Detected: Squat/Lunge. Stand straight.";
|
||||
if (isHandsOverhead) return "Detected: Overhead Press. Keep elbows down.";
|
||||
if (isArmsStraight && !isHandsLow) return "Detected: Lateral Raise. Bend your elbows.";
|
||||
if (isSimultaneous && isElbowsBent) return "Detected: Bicep Curl (Simultaneous). Alternate arms.";
|
||||
return null;
|
||||
}
|
||||
|
||||
if (configKey === 'shoulder_press') {
|
||||
if (isLegsBent) return "Detected: Squat/Lunge. Focus on upper body.";
|
||||
if (isHandsLow && isElbowsBent) return "Detected: Bicep Curl. Push weight UP, not curl.";
|
||||
if (isArmsStraight && !isHandsOverhead) return "Detected: Lateral Raise/Deadlift. Press overhead.";
|
||||
if (isAlternating) return "Detected: Alternating Press. Push both arms together.";
|
||||
return null;
|
||||
}
|
||||
|
||||
if (configKey === 'lateral_raises') {
|
||||
if (isLegsBent) return "Detected: Squat/Lunge. Stand straight.";
|
||||
if (isHandsOverhead) return "Detected: Overhead Press. Stop at shoulder height.";
|
||||
if (isElbowsBent) return "Detected: Bicep/Hammer Curl. Keep arms straight (T-pose).";
|
||||
return null;
|
||||
}
|
||||
|
||||
if (configKey === 'squat') {
|
||||
if (!isLegsBent && isElbowsBent) return "Detected: Bicep/Hammer Curl. Bend your knees!";
|
||||
if (!isLegsBent && isHandsOverhead) return "Detected: Overhead Press. Focus on legs.";
|
||||
const diffKnee = Math.abs(features.leftKnee - features.rightKnee);
|
||||
if (isLegsBent && diffKnee > 30) return "Detected: Lunge. Keep knees symmetrical for Squat.";
|
||||
return null;
|
||||
}
|
||||
|
||||
if (configKey === 'lunges') {
|
||||
if (!isLegsBent && isElbowsBent) return "Detected: Curl. Focus on legs.";
|
||||
if (!isLegsBent && isHandsOverhead) return "Detected: Press. Focus on legs.";
|
||||
const diffKnee = Math.abs(features.leftKnee - features.rightKnee);
|
||||
if (isLegsBent && diffKnee < 15) return "Detected: Squat. Step one foot back for Lunge.";
|
||||
return null;
|
||||
}
|
||||
|
||||
if (configKey === 'deadlift') {
|
||||
const isHipsBent = features.leftHip < 140 || features.rightHip < 140;
|
||||
if (!isHipsBent && isElbowsBent) return "Detected: Curl. Keep arms straight/locked.";
|
||||
if (!isHipsBent && isHandsOverhead) return "Detected: Press. Keep bar low.";
|
||||
if (!isHipsBent && isLegsBent) return "Detected: Squat (Too much knee). Hinge at hips more.";
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private calculateDeviation(
|
||||
configKey: string,
|
||||
features: any,
|
||||
fsmState: "LOW" | "HIGH"
|
||||
): { mae: number; isDeviating: boolean; details: string[] } {
|
||||
|
||||
const config = EXERCISE_CONFIGS[configKey];
|
||||
|
||||
if (!config || !config.dynamic_angles) {
|
||||
return { mae: 0, isDeviating: false, details: [] };
|
||||
}
|
||||
|
||||
let targetSuffix = '';
|
||||
if (config.phase_type === 'start_down') {
|
||||
targetSuffix = (fsmState === 'HIGH') ? '_up' : '_down';
|
||||
} else {
|
||||
targetSuffix = (fsmState === 'HIGH') ? '_down' : '_up';
|
||||
}
|
||||
|
||||
const errors: number[] = [];
|
||||
const details: string[] = [];
|
||||
|
||||
Object.keys(config.dynamic_angles).forEach(key => {
|
||||
if (key.endsWith(targetSuffix)) {
|
||||
const prefix = key.replace(targetSuffix, '');
|
||||
let val = 0;
|
||||
|
||||
// --- FIX: Specific Handling for Overhead Press & Curls (Dual Arm Checks) ---
|
||||
if ((configKey === 'bicep_curl' || configKey === 'hammer_curl' || configKey === 'shoulder_press') && prefix === 'elbow') {
|
||||
const errL = calculateRangeDeviation(features.leftElbow, config.dynamic_angles[key]);
|
||||
const errR = calculateRangeDeviation(features.rightElbow, config.dynamic_angles[key]);
|
||||
errors.push(errL, errR);
|
||||
if(errL > 0) details.push(`L_${key} dev ${errL.toFixed(0)}`);
|
||||
if(errR > 0) details.push(`R_${key} dev ${errR.toFixed(0)}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Standard Averaging Logic (As requested)
|
||||
if (prefix.includes('elbow')) val = (features.leftElbow + features.rightElbow) / 2;
|
||||
else if (prefix.includes('knee')) val = (features.leftKnee + features.rightKnee) / 2;
|
||||
else if (prefix.includes('hip')) val = (features.leftHip + features.rightHip) / 2;
|
||||
else if (prefix.includes('shoulder')) val = (features.leftShoulderY * 180);
|
||||
|
||||
if(val > 0) {
|
||||
const err = calculateRangeDeviation(val, config.dynamic_angles[key]);
|
||||
errors.push(err);
|
||||
if(err > 0) details.push(`${key} dev ${err.toFixed(0)}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const mae = computeMAE(errors);
|
||||
|
||||
return {
|
||||
mae,
|
||||
isDeviating: mae > this.DEVIATION_THRESHOLD,
|
||||
details
|
||||
};
|
||||
}
|
||||
|
||||
public process(exerciseName: string, landmarks: Landmark[], worldLandmarks: Landmark[] = [], frameTime: number = 0) {
|
||||
// // Normalize exercise name
|
||||
// const KEY_MAP: {[key:string]: string} = {
|
||||
// 'bicep_curls': 'bicep_curl',
|
||||
// 'shoulder_press': 'shoulder_press',
|
||||
// 'hammer_curls': 'hammer_curl',
|
||||
// 'lateral_raises': 'lateral_raises',
|
||||
// 'squats': 'squat',
|
||||
// 'deadlifts': 'deadlift',
|
||||
// 'lunges': 'lunges'
|
||||
// };
|
||||
const configKey = normalizeExerciseName(exerciseName);
|
||||
|
||||
// Init counters if not exists
|
||||
if (!this.counters[configKey]) {
|
||||
const factory = COUNTER_MAP[configKey];
|
||||
if (factory) {
|
||||
console.log(`RehabCore: Initialized counter for ${configKey}`);
|
||||
this.counters[configKey] = factory();
|
||||
} else {
|
||||
console.warn(`RehabCore: No factory found for exercise "${configKey}" (Raw: ${exerciseName})`);
|
||||
return null; // Unknown exercise
|
||||
}
|
||||
}
|
||||
|
||||
const activeCounters = this.counters[configKey];
|
||||
if (!activeCounters) return null;
|
||||
|
||||
// Data Conversion
|
||||
// We usually need World Landmarks for accurate angles (meters).
|
||||
// MediaPipe Pose returns:
|
||||
// 1. poseLandmarks (normalized x,y,z)
|
||||
// 2. poseWorldLandmarks (meters x,y,z)
|
||||
//
|
||||
// The current `landmarks` input in Straps usually comes from `poseLandmarks` (normalized).
|
||||
// The new algorithm expects `normalized` AND `world`.
|
||||
// If we only have normalized, we can pass normalized as world, but angles might be skewed by perspective.
|
||||
// HOWEVER, `angleDeg` uses `sub` and `dot`. If z is normalized (0..1 scale relative to image width), it's roughly ok for basic 2D-ish angles.
|
||||
// Ideally we update `HARCore` to pass world landmarks too.
|
||||
// For now, I will use `landmarks` for BOTH, assuming the user is aware or `z` is roughly scaled.
|
||||
// Actually `HARCore` sees `Landmark` interface which has x,y,z.
|
||||
|
||||
const vecLandmarks: Vec3[] = landmarks.map(l => ({ x: l.x, y: l.y, z: l.z || 0, visibility: l.visibility }));
|
||||
const vecWorld: Vec3[] = (worldLandmarks && worldLandmarks.length > 0)
|
||||
? worldLandmarks.map(l => ({ x: l.x, y: l.y, z: l.z || 0, visibility: l.visibility }))
|
||||
: vecLandmarks; // Fallback
|
||||
|
||||
// Compute Features
|
||||
const features = computeFeatures(vecLandmarks, vecWorld, frameTime || Date.now());
|
||||
|
||||
// Update Counters
|
||||
const results = activeCounters.map(c => c.update(features));
|
||||
|
||||
// Determine dominant state (Use the first counter as primary reference)
|
||||
const mainCounter = this.counters[configKey]?.[0];
|
||||
const fsmState = mainCounter ? mainCounter.state : "LOW";
|
||||
|
||||
// Calculate Deviation
|
||||
const deviationAnalysis = this.calculateDeviation(exerciseName, features, fsmState);
|
||||
|
||||
const wrongExerciseWarning = this.validateExerciseType(configKey, features);
|
||||
// Format Output for HARCore
|
||||
// Old format: { left: { stage, reps, angle }, right: { stage, reps, angle }, feedback, scores }
|
||||
|
||||
// Determine Left/Right results
|
||||
// If we have 2 counters, usually [0]=Left, [1]=Right (based on my factory above)
|
||||
// Wait, BicepCurlCounter('left') is first?
|
||||
// Let's look at factory:
|
||||
// 'bicep_curl': () => [new BicepCurlCounter('left'), new BicepCurlCounter('right')],
|
||||
|
||||
let leftRes = { stage: 'REST', reps: 0, angle: 0 };
|
||||
let rightRes = { stage: 'REST', reps: 0, angle: 0 };
|
||||
let feedback = "";
|
||||
|
||||
if (configKey === 'bicep_curl' || configKey === 'hammer_curl') {
|
||||
const lCounter = activeCounters[0];
|
||||
const rCounter = activeCounters[1];
|
||||
|
||||
leftRes = {
|
||||
stage: lCounter.state === 'HIGH' ? 'UP' : 'DOWN',
|
||||
reps: lCounter.reps,
|
||||
angle: features.leftElbow
|
||||
};
|
||||
rightRes = {
|
||||
stage: rCounter.state === 'HIGH' ? 'UP' : 'DOWN',
|
||||
reps: rCounter.reps,
|
||||
angle: features.rightElbow
|
||||
};
|
||||
} else {
|
||||
// Unified counters (Squat, Press, etc)
|
||||
// We apply result to "Both" or just map to nice UI
|
||||
const main = activeCounters[0];
|
||||
const stage = main.state === 'HIGH' ? 'UP' : 'DOWN';
|
||||
const reps = main.reps;
|
||||
|
||||
leftRes = { stage, reps, angle: 0 }; // Angle 0 for now as main metric might be diff
|
||||
rightRes = { stage, reps, angle: 0 };
|
||||
|
||||
// Populate specific angles for UI if needed
|
||||
if (configKey === 'squat') { leftRes.angle = features.leftKnee; rightRes.angle = features.rightKnee; }
|
||||
if (configKey === 'shoulder_press') { leftRes.angle = features.leftElbow; rightRes.angle = features.rightElbow; } // Approx
|
||||
}
|
||||
|
||||
if (wrongExerciseWarning) {
|
||||
feedback = `⚠️ ${wrongExerciseWarning}`;
|
||||
}
|
||||
|
||||
// Append Deviation info to feedback
|
||||
else if (deviationAnalysis.isDeviating) {
|
||||
const detailText = deviationAnalysis.details.join(", ");
|
||||
feedback += ` | Fix Form: ${detailText}`;
|
||||
}
|
||||
// Accumulate feedback? FSM has `debug`
|
||||
// results.forEach(r => if(r.debug.note) feedback += r.debug.note + " ");
|
||||
|
||||
return {
|
||||
left: leftRes,
|
||||
right: rightRes,
|
||||
feedback: feedback.trim(),
|
||||
scores: {deviation_mae: deviationAnalysis.mae} // No scores in new FSM yet
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
public getReps(exName: string) {
|
||||
// // Normalized key
|
||||
// const KEY_MAP: {[key:string]: string} = {
|
||||
// 'bicep_curls': 'bicep_curl',
|
||||
// 'shoulder_press': 'shoulder_press',
|
||||
// 'hammer_curls': 'hammer_curl',
|
||||
// 'lateral_raises': 'lateral_raises',
|
||||
// 'squats': 'squat',
|
||||
// 'deadlifts': 'deadlift',
|
||||
// 'lunges': 'lunges'
|
||||
// };
|
||||
const configKey = normalizeExerciseName(exName);
|
||||
const counters = this.counters[configKey];
|
||||
if (!counters || counters.length === 0) return 0;
|
||||
|
||||
if (configKey === 'hammer_curl') {
|
||||
return Math.min(...counters.map(c => c.reps));
|
||||
}
|
||||
|
||||
// If multiple counters (bilateral), usually we return the SUM or MAX or MIN?
|
||||
// Old logic was: wait for both to complete -> increment total.
|
||||
// New FSM logic tracks reps independently.
|
||||
// For Curls, it likely makes sense to show Total Reps (L+R) or Max?
|
||||
// Usually "1 Rep" means both arms if simultaneous, or 1 each.
|
||||
// For now, let's return the AVG or MAX.
|
||||
// If unilateral exercise mode?
|
||||
// Straps usually assumes bilateral simultaneous.
|
||||
// If I do 10 left and 10 right = 20 total? or 10 sets?
|
||||
// Let's return MAX for now (assuming users try to keep sync).
|
||||
// Actually, if I do alternate curls, I want sum?
|
||||
// Let's stick to MAX for synchronized exercises.
|
||||
return Math.max(...counters.map(c => c.reps));
|
||||
}
|
||||
}
|
||||
|
||||
373
lib/pose/RehabFSM.ts
Normal file
|
|
@ -0,0 +1,373 @@
|
|||
|
||||
export type Vec3 = { x: number; y: number; z: number; visibility?: number };
|
||||
|
||||
export const LM = {
|
||||
NOSE: 0,
|
||||
LEFT_SHOULDER: 11,
|
||||
RIGHT_SHOULDER: 12,
|
||||
LEFT_ELBOW: 13,
|
||||
RIGHT_ELBOW: 14,
|
||||
LEFT_WRIST: 15,
|
||||
RIGHT_WRIST: 16,
|
||||
LEFT_PINKY: 17, // NEW
|
||||
RIGHT_PINKY: 18, // NEW
|
||||
LEFT_INDEX: 19,
|
||||
RIGHT_INDEX: 20,
|
||||
LEFT_THUMB: 21, // NEW
|
||||
RIGHT_THUMB: 22, // NEW
|
||||
LEFT_HIP: 23,
|
||||
RIGHT_HIP: 24,
|
||||
LEFT_KNEE: 25,
|
||||
RIGHT_KNEE: 26,
|
||||
LEFT_ANKLE: 27,
|
||||
RIGHT_ANKLE: 28,
|
||||
} as const;
|
||||
|
||||
function clamp(x: number, a: number, b: number) { return Math.max(a, Math.min(b, x)); }
|
||||
|
||||
function sub(a: Vec3, b: Vec3) { return { x: a.x - b.x, y: a.y - b.y, z: a.z - b.z }; }
|
||||
function dot(a: Vec3, b: Vec3) { return a.x*b.x + a.y*b.y + a.z*b.z; }
|
||||
function norm(a: Vec3) { return Math.sqrt(dot(a,a)) + 1e-8; }
|
||||
|
||||
export function angleDeg(A: Vec3, B: Vec3, C: Vec3): number {
|
||||
const BA = sub(A,B);
|
||||
const BC = sub(C,B);
|
||||
const cos = clamp(dot(BA,BC) / (norm(BA)*norm(BC)), -1, 1);
|
||||
return Math.acos(cos) * 180 / Math.PI;
|
||||
}
|
||||
|
||||
function ema(prev: number | null, x: number, alpha: number): number {
|
||||
return prev === null ? x : alpha*x + (1-alpha)*prev;
|
||||
}
|
||||
|
||||
function meanVisibility(lms: Vec3[], idxs: number[]): number {
|
||||
const v = idxs.map(i => (lms[i]?.visibility ?? 1.0));
|
||||
const s = v.reduce((a,b)=>a+b, 0);
|
||||
return v.length ? s / v.length : 0;
|
||||
}
|
||||
|
||||
export type PoseFeatures = {
|
||||
tMs: number;
|
||||
leftElbow: number; rightElbow: number;
|
||||
leftKnee: number; rightKnee: number;
|
||||
leftHip: number; rightHip: number;
|
||||
|
||||
// normalized coords (0..1), y lebih besar = lebih bawah
|
||||
leftWristY: number; rightWristY: number;
|
||||
leftShoulderY: number; rightShoulderY: number;
|
||||
noseY: number;
|
||||
|
||||
// NEW: Hand Orientation Data
|
||||
leftThumbY: number; leftPinkyY: number;
|
||||
rightThumbY: number; rightPinkyY: number;
|
||||
|
||||
// NEW: X Coordinates for Width/Rotation Check
|
||||
leftThumbX: number; leftPinkyX: number;
|
||||
rightThumbX: number; rightPinkyX: number;
|
||||
|
||||
visArms: number; visLegs: number;
|
||||
};
|
||||
|
||||
export function computeFeatures(
|
||||
normalized: Vec3[], // landmarks (image coords normalized)
|
||||
world: Vec3[], // worldLandmarks (meters)
|
||||
tMs: number
|
||||
): PoseFeatures {
|
||||
const A = (i: number) => world[i];
|
||||
const N = (i: number) => normalized[i];
|
||||
|
||||
const leftElbow = angleDeg(A(LM.LEFT_SHOULDER), A(LM.LEFT_ELBOW), A(LM.LEFT_WRIST));
|
||||
const rightElbow = angleDeg(A(LM.RIGHT_SHOULDER), A(LM.RIGHT_ELBOW), A(LM.RIGHT_WRIST));
|
||||
|
||||
const leftKnee = angleDeg(A(LM.LEFT_HIP), A(LM.LEFT_KNEE), A(LM.LEFT_ANKLE));
|
||||
const rightKnee = angleDeg(A(LM.RIGHT_HIP), A(LM.RIGHT_KNEE), A(LM.RIGHT_ANKLE));
|
||||
|
||||
const leftHip = angleDeg(A(LM.LEFT_SHOULDER), A(LM.LEFT_HIP), A(LM.LEFT_KNEE));
|
||||
const rightHip = angleDeg(A(LM.RIGHT_SHOULDER), A(LM.RIGHT_HIP), A(LM.RIGHT_KNEE));
|
||||
|
||||
const armsIdx = [LM.LEFT_SHOULDER, LM.LEFT_ELBOW, LM.LEFT_WRIST, LM.RIGHT_SHOULDER, LM.RIGHT_ELBOW, LM.RIGHT_WRIST];
|
||||
const legsIdx = [LM.LEFT_HIP, LM.LEFT_KNEE, LM.LEFT_ANKLE, LM.RIGHT_HIP, LM.RIGHT_KNEE, LM.RIGHT_ANKLE];
|
||||
|
||||
return {
|
||||
tMs,
|
||||
leftElbow, rightElbow,
|
||||
leftKnee, rightKnee,
|
||||
leftHip, rightHip,
|
||||
|
||||
leftWristY: N(LM.LEFT_WRIST).y,
|
||||
rightWristY: N(LM.RIGHT_WRIST).y,
|
||||
leftShoulderY: N(LM.LEFT_SHOULDER).y,
|
||||
rightShoulderY: N(LM.RIGHT_SHOULDER).y,
|
||||
noseY: N(LM.NOSE).y,
|
||||
|
||||
// Capture Thumb/Pinky Y for grip check
|
||||
leftThumbY: N(LM.LEFT_THUMB).y,
|
||||
leftPinkyY: N(LM.LEFT_PINKY).y,
|
||||
rightThumbY: N(LM.RIGHT_THUMB).y,
|
||||
rightPinkyY: N(LM.RIGHT_PINKY).y,
|
||||
|
||||
// NEW: Capture X coordinates
|
||||
leftThumbX: N(LM.LEFT_THUMB).x,
|
||||
leftPinkyX: N(LM.LEFT_PINKY).x,
|
||||
rightThumbX: N(LM.RIGHT_THUMB).x,
|
||||
rightPinkyX: N(LM.RIGHT_PINKY).x,
|
||||
|
||||
visArms: meanVisibility(world, armsIdx),
|
||||
visLegs: meanVisibility(world, legsIdx),
|
||||
};
|
||||
}
|
||||
|
||||
// =======================
|
||||
// Robust FSM base
|
||||
// =======================
|
||||
export class RepFSM {
|
||||
public state: "LOW" | "HIGH" = "LOW";
|
||||
public reps = 0;
|
||||
|
||||
private metricS: number | null = null;
|
||||
private metricPrev: number | null = null;
|
||||
private lastMotionT: number | null = null;
|
||||
|
||||
private enteredHighT: number | null = null;
|
||||
private cycleStartT: number | null = null;
|
||||
private cycleMin: number | null = null;
|
||||
private cycleMax: number | null = null;
|
||||
|
||||
constructor(
|
||||
public name: string,
|
||||
public minVis = 0.6,
|
||||
public emaAlpha = 0.25,
|
||||
public idleVelTh = 0.8,
|
||||
public idleMs = 900,
|
||||
public highHoldMs = 120,
|
||||
public minRepMs = 500,
|
||||
public maxRepMs = 12000,
|
||||
public minRomDeg = 60,
|
||||
) {}
|
||||
|
||||
visibilityOk(_f: PoseFeatures): boolean { return true; }
|
||||
metric(_f: PoseFeatures): number { throw new Error("metric not implemented"); }
|
||||
isLow(_m: number, _f: PoseFeatures): boolean { throw new Error("isLow not implemented"); }
|
||||
isHigh(_m: number, _f: PoseFeatures): boolean { throw new Error("isHigh not implemented"); }
|
||||
extraValid(_f: PoseFeatures): boolean { return true; }
|
||||
|
||||
private updateRom(m: number) {
|
||||
this.cycleMin = this.cycleMin === null ? m : Math.min(this.cycleMin, m);
|
||||
this.cycleMax = this.cycleMax === null ? m : Math.max(this.cycleMax, m);
|
||||
}
|
||||
|
||||
update(f: PoseFeatures): { delta: number; debug: any } {
|
||||
const t = f.tMs;
|
||||
if (!this.visibilityOk(f)) {
|
||||
return { delta: 0, debug: { name: this.name, state: this.state, note: "visibility_fail" } };
|
||||
}
|
||||
|
||||
const mRaw = this.metric(f);
|
||||
this.metricS = ema(this.metricS, mRaw, this.emaAlpha);
|
||||
|
||||
if (this.metricPrev === null) {
|
||||
this.metricPrev = this.metricS;
|
||||
this.lastMotionT = t;
|
||||
return { delta: 0, debug: { name: this.name, state: this.state, m: this.metricS } };
|
||||
}
|
||||
|
||||
const vel = Math.abs(this.metricS - this.metricPrev);
|
||||
this.metricPrev = this.metricS;
|
||||
|
||||
if (vel >= this.idleVelTh) this.lastMotionT = t;
|
||||
|
||||
if (this.lastMotionT !== null && (t - this.lastMotionT) > this.idleMs) {
|
||||
// idle -> reset to safe state
|
||||
this.state = "LOW";
|
||||
this.enteredHighT = null;
|
||||
this.cycleStartT = null;
|
||||
this.cycleMin = null;
|
||||
this.cycleMax = null;
|
||||
return { delta: 0, debug: { name: this.name, state: this.state, note: "idle" } };
|
||||
}
|
||||
|
||||
this.updateRom(this.metricS);
|
||||
|
||||
if (this.state === "LOW") {
|
||||
if (this.isHigh(this.metricS, f) && this.extraValid(f)) {
|
||||
this.state = "HIGH";
|
||||
this.enteredHighT = t;
|
||||
if (this.cycleStartT === null) this.cycleStartT = t;
|
||||
}
|
||||
return { delta: 0, debug: { name: this.name, state: this.state, m: this.metricS } };
|
||||
}
|
||||
|
||||
// state HIGH
|
||||
if (this.enteredHighT !== null && (t - this.enteredHighT) < this.highHoldMs) {
|
||||
return { delta: 0, debug: { name: this.name, state: this.state, m: this.metricS, note: "hold_high" } };
|
||||
}
|
||||
|
||||
if (this.isLow(this.metricS, f)) {
|
||||
const dur = this.cycleStartT ? (t - this.cycleStartT) : 0;
|
||||
const rom = (this.cycleMax !== null && this.cycleMin !== null) ? (this.cycleMax - this.cycleMin) : 0;
|
||||
const okDur = dur >= this.minRepMs && dur <= this.maxRepMs;
|
||||
const okRom = rom >= this.minRomDeg;
|
||||
|
||||
let delta = 0;
|
||||
if (okDur && okRom) {
|
||||
this.reps += 1;
|
||||
delta = 1;
|
||||
}
|
||||
|
||||
// reset
|
||||
this.state = "LOW";
|
||||
this.enteredHighT = null;
|
||||
this.cycleStartT = null;
|
||||
this.cycleMin = null;
|
||||
this.cycleMax = null;
|
||||
|
||||
return { delta, debug: { name: this.name, state: this.state, m: this.metricS, dur, rom, okDur, okRom } };
|
||||
}
|
||||
|
||||
return { delta: 0, debug: { name: this.name, state: this.state, m: this.metricS } };
|
||||
}
|
||||
}
|
||||
|
||||
// =======================
|
||||
// Exercise counters
|
||||
// =======================
|
||||
// --- Base Class for Curls ---
|
||||
class BaseCurlCounter extends RepFSM {
|
||||
constructor(public side: "left" | "right", name: string) {
|
||||
super(name, 0.6, 0.25, 0.8, 900, 120, 500, 12000, 70);
|
||||
}
|
||||
private highTh = 85;
|
||||
private lowTh = 140;
|
||||
|
||||
visibilityOk(f: PoseFeatures) { return f.visArms >= this.minVis; }
|
||||
metric(f: PoseFeatures) { return this.side === "right" ? f.rightElbow : f.leftElbow; }
|
||||
isLow(m: number) { return m >= this.lowTh; }
|
||||
isHigh(m: number) { return m <= this.highTh; }
|
||||
}
|
||||
|
||||
export class BicepCurlCounter extends BaseCurlCounter {
|
||||
constructor(side: "left" | "right" = "right") {
|
||||
super(side, "bicep_curl");
|
||||
}
|
||||
|
||||
// LOGIC: Simultaneous Lift
|
||||
// Valid ONLY if the OTHER arm is also active (Bent).
|
||||
extraValid(f: PoseFeatures) {
|
||||
const otherArmAngle = this.side === "right" ? f.leftElbow : f.rightElbow;
|
||||
|
||||
// Check: Is the other arm bent? (< 120 degrees)
|
||||
// If the other arm is straight (> 120), then we are NOT doing simultaneous curls.
|
||||
if (otherArmAngle > 120) {
|
||||
return false; // Reject: Other arm is lazy/resting
|
||||
}
|
||||
return true; // Accept: Both arms are working
|
||||
}
|
||||
}
|
||||
|
||||
export class HammerCurlCounter extends BaseCurlCounter {
|
||||
constructor(side: "left" | "right" = "right") {
|
||||
super(side, "hammer_curl");
|
||||
}
|
||||
|
||||
// LOGIC: Alternating Lift
|
||||
// Valid ONLY if the OTHER arm is resting (Straight).
|
||||
extraValid(f: PoseFeatures) {
|
||||
const otherArmAngle = this.side === "right" ? f.leftElbow : f.rightElbow;
|
||||
|
||||
// Check: Is the other arm straight? (> 130 degrees)
|
||||
// If the other arm is bent (< 130), it means we are moving both (Simultaneous).
|
||||
if (otherArmAngle < 130) {
|
||||
return false; // Reject: Both arms are moving
|
||||
}
|
||||
return true; // Accept: Only this arm is moving
|
||||
}
|
||||
}
|
||||
|
||||
export class OverheadPressCounter extends RepFSM {
|
||||
constructor() {
|
||||
// Using relaxed Min ROM (30) to ensuring counting
|
||||
super("overhead_press", 0.6, 0.25, 0.8, 900, 120, 500, 12000, 30);
|
||||
}
|
||||
|
||||
visibilityOk(f: PoseFeatures) { return f.visArms >= this.minVis; }
|
||||
|
||||
metric(f: PoseFeatures) {
|
||||
// Use MIN elbow angle to require at least one arm to lock out
|
||||
return Math.min(f.leftElbow, f.rightElbow);
|
||||
}
|
||||
|
||||
// High State (Lockout) > 140 deg
|
||||
isHigh(m: number) {
|
||||
return m > 150;
|
||||
}
|
||||
|
||||
// Low State (Start/Chin level) < 120 deg
|
||||
isLow(m: number) {
|
||||
return m < 140;
|
||||
}
|
||||
|
||||
// Validation: Ensure hands are ABOVE shoulders
|
||||
extraValid(f: PoseFeatures) {
|
||||
const isHandsUpL = f.leftWristY < f.leftShoulderY;
|
||||
const isHandsUpR = f.rightWristY < f.rightShoulderY;
|
||||
return isHandsUpL && isHandsUpR;
|
||||
}
|
||||
}
|
||||
|
||||
export class LateralRaiseCounter extends RepFSM {
|
||||
private highMargin = 0.05;
|
||||
private lowMargin = 0.12;
|
||||
constructor() {
|
||||
super("lateral_raise", 0.6, 0.25, 0.003, 900, 120, 500, 12000, 0);
|
||||
}
|
||||
visibilityOk(f: PoseFeatures) { return f.visArms >= this.minVis; }
|
||||
metric(f: PoseFeatures) {
|
||||
const dl = Math.abs(f.leftWristY - f.leftShoulderY);
|
||||
const dr = Math.abs(f.rightWristY - f.rightShoulderY);
|
||||
return 0.5*(dl+dr);
|
||||
}
|
||||
isHigh(m: number) { return m < this.highMargin; }
|
||||
isLow(m: number) { return m > this.lowMargin; }
|
||||
extraValid(f: PoseFeatures) { return f.leftElbow > 120 && f.rightElbow > 120; }
|
||||
}
|
||||
|
||||
export class SquatCounter extends RepFSM {
|
||||
private topTh = 165;
|
||||
private bottomTh = 100;
|
||||
constructor() { super("squat", 0.6, 0.25, 0.8, 900, 120, 600, 12000, 40); }
|
||||
visibilityOk(f: PoseFeatures) { return f.visLegs >= this.minVis; }
|
||||
metric(f: PoseFeatures) { return Math.min(f.leftKnee, f.rightKnee); }
|
||||
isLow(m: number) { return m >= this.topTh; } // standing
|
||||
isHigh(m: number) { return m <= this.bottomTh; } // bottom
|
||||
extraValid(f: PoseFeatures) { return Math.min(f.leftHip, f.rightHip) < 140; }
|
||||
}
|
||||
|
||||
export class DeadliftCounter extends RepFSM {
|
||||
private topHipTh = 165;
|
||||
private bottomHipTh = 120;
|
||||
constructor() { super("deadlift", 0.6, 0.25, 0.8, 900, 120, 600, 12000, 35); }
|
||||
visibilityOk(f: PoseFeatures) { return f.visLegs >= this.minVis && f.visArms >= 0.4; }
|
||||
metric(f: PoseFeatures) { return Math.min(f.leftHip, f.rightHip); }
|
||||
isLow(m: number) { return m >= this.topHipTh; }
|
||||
isHigh(m: number, f: PoseFeatures) {
|
||||
const knee = Math.min(f.leftKnee, f.rightKnee);
|
||||
return m <= this.bottomHipTh && knee > 110;
|
||||
}
|
||||
}
|
||||
|
||||
export class LungeCounter extends RepFSM {
|
||||
private topKneeTh = 165;
|
||||
private bottomFrontTh = 105;
|
||||
private bottomBackTh = 130;
|
||||
constructor() { super("lunge", 0.6, 0.25, 0.8, 900, 120, 600, 12000, 25); }
|
||||
visibilityOk(f: PoseFeatures) { return f.visLegs >= this.minVis; }
|
||||
metric(f: PoseFeatures) { return Math.min(f.leftKnee, f.rightKnee); }
|
||||
isLow(_m: number, f: PoseFeatures) {
|
||||
return f.leftKnee > this.topKneeTh && f.rightKnee > this.topKneeTh;
|
||||
}
|
||||
isHigh(_m: number, f: PoseFeatures) {
|
||||
const front = Math.min(f.leftKnee, f.rightKnee);
|
||||
const back = Math.max(f.leftKnee, f.rightKnee);
|
||||
return front < this.bottomFrontTh && back < this.bottomBackTh;
|
||||
}
|
||||
}
|
||||
392
lib/pose/RepetitionCounter.ts
Normal file
|
|
@ -0,0 +1,392 @@
|
|||
|
||||
import { Landmark, AnglesDict } from './ExerciseRules';
|
||||
import {
|
||||
computeConvexHullArea,
|
||||
normalizeLandmarks,
|
||||
inRange,
|
||||
calculateContainmentScore
|
||||
} from './MathUtils';
|
||||
|
||||
// --- Types ---
|
||||
export type RepData = {
|
||||
elbow_r: { up: number[], down: number[] };
|
||||
elbow_l: { up: number[], down: number[] };
|
||||
shoulder_r: { up: number[], down: number[] };
|
||||
shoulder_l: { up: number[], down: number[] };
|
||||
hull_area: { up: number[], down: number[] };
|
||||
wrist_dist: number[];
|
||||
static_angles: { [key: string]: number[] };
|
||||
feedback: string[];
|
||||
frame_times: number[];
|
||||
};
|
||||
|
||||
export type RepetitionSummary = {
|
||||
scores: any;
|
||||
feedback: string;
|
||||
fps: number;
|
||||
count?: number;
|
||||
};
|
||||
|
||||
export class RepetitionCounter {
|
||||
// Buffers (Max len 15)
|
||||
private elbow_hist_r: number[] = [];
|
||||
private elbow_hist_l: number[] = [];
|
||||
private shoulder_hist_r: number[] = [];
|
||||
private shoulder_hist_l: number[] = [];
|
||||
|
||||
// State
|
||||
public current_exercise: string = "unknown";
|
||||
public stage_right: string | null = null;
|
||||
public stage_left: string | null = null;
|
||||
|
||||
// Phase State Machine
|
||||
private right_phase: "idle" | "down_prep" | "up" | "done" = "idle";
|
||||
private left_phase: "idle" | "down_prep" | "up" | "done" = "idle";
|
||||
private hull_phase: "idle" | "down" | "up" = "idle";
|
||||
|
||||
// Raw Reps
|
||||
private raw_reps: { [key: string]: number } = {
|
||||
"hammer_curl": 0,
|
||||
"overhead_press": 0
|
||||
};
|
||||
private raw_right_phase: "idle" | "down_prep" | "up" | "done" = "idle";
|
||||
private raw_left_phase: "idle" | "down_prep" | "up" | "done" = "idle";
|
||||
|
||||
// Debounce / Bad Frame Tolerance
|
||||
private bad_frame_count_r = 0;
|
||||
private bad_frame_count_l = 0;
|
||||
|
||||
// Data Collection
|
||||
private rep_data: RepData;
|
||||
public all_scores: any[] = [];
|
||||
public last_score: any = {};
|
||||
|
||||
constructor() {
|
||||
this.rep_data = this._reset_rep_data();
|
||||
}
|
||||
|
||||
private _reset_rep_data(): RepData {
|
||||
return {
|
||||
elbow_r: { up: [], down: [] }, elbow_l: { up: [], down: [] },
|
||||
shoulder_r: { up: [], down: [] }, shoulder_l: { up: [], down: [] },
|
||||
hull_area: { up: [], down: [] },
|
||||
wrist_dist: [],
|
||||
static_angles: {
|
||||
knee_r: [], knee_l: [],
|
||||
hip_r: [], hip_l: [],
|
||||
shoulder_r: [], shoulder_l: []
|
||||
},
|
||||
feedback: [],
|
||||
frame_times: []
|
||||
};
|
||||
}
|
||||
|
||||
private updateBuffer(buffer: number[], val: number) {
|
||||
buffer.push(val);
|
||||
if (buffer.length > 15) buffer.shift();
|
||||
}
|
||||
|
||||
public update_angles(elbow_r: number, elbow_l: number, shoulder_r: number, shoulder_l: number) {
|
||||
this.updateBuffer(this.elbow_hist_r, elbow_r);
|
||||
this.updateBuffer(this.shoulder_hist_r, shoulder_r);
|
||||
this.updateBuffer(this.elbow_hist_l, elbow_l);
|
||||
this.updateBuffer(this.shoulder_hist_l, shoulder_l);
|
||||
}
|
||||
|
||||
public get_raw_reps(exercise_name: string): number {
|
||||
return this.raw_reps[exercise_name] || 0;
|
||||
}
|
||||
|
||||
// --- Scoring Logic (Ported) ---
|
||||
private _calculate_dynamic_angle_score(
|
||||
dynamic_thresholds: any,
|
||||
relevant_joints: string[],
|
||||
buffer: number
|
||||
): { [key: string]: number } {
|
||||
const joint_scores: { [key: string]: number } = {};
|
||||
|
||||
for (const joint of relevant_joints) {
|
||||
const joint_base = joint.split('_')[0]; // e.g., elbow_r -> elbow
|
||||
|
||||
for (const stage of ['up', 'down'] as const) {
|
||||
// Access rep_data dynamically.
|
||||
// Note: Typescript dynamic access requires careful typing or casting.
|
||||
const user_angles = (this.rep_data as any)[joint]?.[stage] as number[];
|
||||
const score_key = `${joint}_${stage}`;
|
||||
|
||||
if (user_angles && user_angles.length > 0) {
|
||||
const user_min = Math.min(...user_angles);
|
||||
const user_max = Math.max(...user_angles);
|
||||
|
||||
const ref_key = `${joint_base}_${stage}`; // e.g. elbow_up
|
||||
const ref_range = dynamic_thresholds[ref_key] || [0, 0];
|
||||
const [ref_min, ref_max] = ref_range;
|
||||
|
||||
const out_low = Math.max(0, ref_min - user_min);
|
||||
const out_high = Math.max(0, user_max - ref_max);
|
||||
|
||||
const pen_low = Math.max(0, out_low - buffer);
|
||||
const pen_high = Math.max(0, out_high - buffer);
|
||||
|
||||
const total_penalty = pen_low + pen_high;
|
||||
const user_length = user_max - user_min;
|
||||
|
||||
let score = 0.0;
|
||||
if (user_min >= ref_min && user_max <= ref_max) {
|
||||
score = 1.0;
|
||||
} else if (user_length > 0) {
|
||||
score = Math.max(0, (user_length - total_penalty) / user_length);
|
||||
}
|
||||
|
||||
joint_scores[score_key] = score * 100;
|
||||
}
|
||||
}
|
||||
}
|
||||
return joint_scores;
|
||||
}
|
||||
|
||||
private calculate_repetition_score(
|
||||
config: any,
|
||||
dynamic_thresholds: any,
|
||||
current_exercise: string
|
||||
): any {
|
||||
const gender_thresholds = config; // Assuming full config passed
|
||||
const global_config = { static_angle_tolerance: 12, dynamic_angle_buffer: 10 }; // Defaults
|
||||
|
||||
// 1. Convex Hull
|
||||
const hull_scores: number[] = [];
|
||||
for (const stage of ['up', 'down'] as const) {
|
||||
const user_vals = this.rep_data.hull_area[stage];
|
||||
if (user_vals && user_vals.length > 0) {
|
||||
const user_range: [number, number] = [Math.min(...user_vals), Math.max(...user_vals)];
|
||||
const ref_range = gender_thresholds.convex_hull?.[stage];
|
||||
if (ref_range) {
|
||||
hull_scores.push(calculateContainmentScore(user_range, ref_range));
|
||||
}
|
||||
}
|
||||
}
|
||||
const avg_hull_score = hull_scores.length > 0
|
||||
? hull_scores.reduce((a, b) => a + b, 0) / hull_scores.length
|
||||
: 0;
|
||||
|
||||
// 2. Dynamic Angles
|
||||
const exercise_joints_map: {[key: string]: string[]} = {
|
||||
"hammer_curl": ["elbow_r", "elbow_l"],
|
||||
"overhead_press": ["elbow_r", "elbow_l", "shoulder_r", "shoulder_l"]
|
||||
};
|
||||
const relevant = exercise_joints_map[current_exercise] || [];
|
||||
const dynamic_scores = this._calculate_dynamic_angle_score(
|
||||
dynamic_thresholds,
|
||||
relevant,
|
||||
global_config.dynamic_angle_buffer
|
||||
);
|
||||
|
||||
// 3. Static Angles
|
||||
const static_scores: {[key: string]: number} = {};
|
||||
const ref_static = gender_thresholds.static_angles || {};
|
||||
for (const [joint, ref_val] of Object.entries(ref_static)) {
|
||||
const user_vals = this.rep_data.static_angles[joint];
|
||||
if (user_vals && user_vals.length > 0) {
|
||||
const user_range: [number, number] = [Math.min(...user_vals), Math.max(...user_vals)];
|
||||
const tolerance = global_config.static_angle_tolerance;
|
||||
// ref_val is number, need range [val, val+tol]
|
||||
const r_val = ref_val as number;
|
||||
static_scores[joint] = calculateContainmentScore(user_range, [r_val, r_val + tolerance]) * 100;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Wrist Distance
|
||||
let wrist_score = 0;
|
||||
if (this.rep_data.wrist_dist.length > 0) {
|
||||
const user_vals = this.rep_data.wrist_dist;
|
||||
const user_range: [number, number] = [Math.min(...user_vals), Math.max(...user_vals)];
|
||||
const ref_range = gender_thresholds.wrist_distance;
|
||||
if (ref_range) {
|
||||
wrist_score = calculateContainmentScore(user_range, ref_range);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
"Hull Score": avg_hull_score * 100,
|
||||
"Dynamic Angle Score": dynamic_scores,
|
||||
"Static Angle Score": static_scores,
|
||||
"Wrist Distance Score": wrist_score * 100
|
||||
};
|
||||
}
|
||||
|
||||
// --- Main Logic ---
|
||||
public count_repetitions(
|
||||
angles: AnglesDict,
|
||||
wrist_dist: number,
|
||||
hull_area: number,
|
||||
exercise_config: any,
|
||||
frame_time: number
|
||||
): [string | null, string | null, boolean, RepetitionSummary] {
|
||||
|
||||
let completed = false;
|
||||
let rep_summary: RepetitionSummary = { scores: {}, feedback: '', fps: 0 };
|
||||
|
||||
const thresholds = exercise_config;
|
||||
const dynamic_thresholds = thresholds.dynamic_angles;
|
||||
const phase_type = thresholds.phase_type || 'start_down'; // Default to curl behavior
|
||||
|
||||
if (!dynamic_thresholds) return [this.stage_right, this.stage_left, false, rep_summary];
|
||||
|
||||
// 1. Detect Stages for EACH joint involved
|
||||
// Helper to detect generic stage for a value against a config key
|
||||
const detect_raw = (val: number, key: string) => {
|
||||
// key e.g., 'elbow' -> checks 'elbow_up' and 'elbow_down'
|
||||
const up = dynamic_thresholds[`${key}_up`];
|
||||
const down = dynamic_thresholds[`${key}_down`];
|
||||
if (down && inRange(val, down[0], down[1])) return "down";
|
||||
if (up && inRange(val, up[0], up[1])) return "up";
|
||||
return null;
|
||||
};
|
||||
|
||||
const detect_joint_stage = (joint_prefix: string, side_suffix: string) => {
|
||||
// e.g. joint_prefix='elbow', side_suffix='_r' -> angle 'elbow_r'
|
||||
// check thresholds 'elbow_up', 'elbow_down'
|
||||
const angle = angles[`${joint_prefix}${side_suffix}`];
|
||||
if (angle === undefined) return null;
|
||||
return detect_raw(angle, joint_prefix);
|
||||
};
|
||||
|
||||
// Determine relevant joints from the config keys
|
||||
// e.g. keys: 'elbow_up', 'shoulder_down' -> joints: ['elbow', 'shoulder']
|
||||
const keys = Object.keys(dynamic_thresholds);
|
||||
const joint_prefixes = Array.from(new Set(keys.map(k => k.split('_')[0])));
|
||||
|
||||
// Determine Composite Stage for Right and Left
|
||||
// Logic: All relevant joints must match the target stage to trigger that stage?
|
||||
// OR: At least one matches?
|
||||
// Python logic for HC: "up" if elbow OR shoulder is up. "down" if elbow AND shoulder is down.
|
||||
// Python logic for OV: "up" if shoulder IS up AND matches elbow.
|
||||
|
||||
// Revised Generic Logic:
|
||||
// "UP" = Primary Mover is UP.
|
||||
// "DOWN" = Primary Mover is DOWN.
|
||||
|
||||
// We will define specific "Primary Joints" logic or simple dominant logic.
|
||||
// For simplicity and robustness across these 7 exercises:
|
||||
// - Bicep/Hammer: Elbow is primary.
|
||||
// - Press/Raise: Shoulder & Elbow.
|
||||
// - Squat/Dead/Lunge: Hip & Knee.
|
||||
|
||||
// Let's iterate all tracked joints.
|
||||
// If ANY tracked joint is "up", we lean towards "up".
|
||||
// If ALL tracked joints are "down", we are "down".
|
||||
// (This matches the loose Python logic for 'up' and strict for 'down' in Curls)
|
||||
|
||||
const get_side_stage = (suffix: string) => {
|
||||
const stages = joint_prefixes.map(j => detect_joint_stage(j, suffix));
|
||||
|
||||
// Special overrides based on exercise type if needed, but trying to be generic:
|
||||
if (phase_type === 'start_down') {
|
||||
// E.g. Curl: Start Down.
|
||||
// Up if ANY is Up (e.g. slight shoulder raise + full curl = Up)
|
||||
// Down if ALL are Down (Full extension)
|
||||
if (stages.some(s => s === 'up')) return 'up';
|
||||
if (stages.every(s => s === 'down')) return 'down';
|
||||
} else {
|
||||
// E.g. Squat: Start Up (Standing).
|
||||
// Down (Squatting) if ANY is Down (e.g. hip OR knee flexes deep) -> Actually usually both flex.
|
||||
// Up (Standing) if ALL are Up (Full extension)
|
||||
// Let's invert:
|
||||
if (stages.some(s => s === 'down')) return 'down'; // Dipping
|
||||
if (stages.every(s => s === 'up')) return 'up'; // Standing tall
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
let stage_r = get_side_stage('_r');
|
||||
let stage_l = get_side_stage('_l');
|
||||
|
||||
// Convex Hull Stage (Override/Filter)
|
||||
const ch = thresholds.convex_hull || {};
|
||||
const ch_up = ch.up;
|
||||
const ch_down = ch.down;
|
||||
|
||||
if (inRange(hull_area, ch_down?.[0] || 0, ch_down?.[1] || 0)) this.hull_phase = "down";
|
||||
else if (this.hull_phase === "down" && inRange(hull_area, ch_up?.[0] || 0, ch_up?.[1] || 999)) this.hull_phase = "up";
|
||||
|
||||
// Data Collection
|
||||
const current_stage = this.stage_right || this.stage_left;
|
||||
if (current_stage && (current_stage === 'up' || current_stage === 'down')) {
|
||||
// ... (Data collection remains same) ...
|
||||
this.rep_data.elbow_r[current_stage].push(angles['elbow_r']);
|
||||
this.rep_data.elbow_l[current_stage].push(angles['elbow_l']);
|
||||
this.rep_data.shoulder_r[current_stage].push(angles['shoulder_r']);
|
||||
this.rep_data.shoulder_l[current_stage].push(angles['shoulder_l']);
|
||||
this.rep_data.hull_area[current_stage].push(hull_area);
|
||||
this.rep_data.wrist_dist.push(wrist_dist);
|
||||
this.rep_data.frame_times.push(frame_time);
|
||||
|
||||
const ref_static = thresholds.static_angles || {};
|
||||
for (const joint of Object.keys(ref_static)) {
|
||||
if (this.rep_data.static_angles[joint]) {
|
||||
this.rep_data.static_angles[joint].push(angles[joint] || 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Debounce / Smoothing
|
||||
const bad_limit = 5;
|
||||
if (stage_r) { this.stage_right = stage_r; this.bad_frame_count_r = 0; }
|
||||
else { this.bad_frame_count_r++; if (this.bad_frame_count_r >= bad_limit) this.stage_right = null; }
|
||||
|
||||
if (stage_l) { this.stage_left = stage_l; this.bad_frame_count_l = 0; }
|
||||
else { this.bad_frame_count_l++; if (this.bad_frame_count_l >= bad_limit) this.stage_left = null; }
|
||||
|
||||
// --- State Machine (Generic) ---
|
||||
// We track "Right" and "Left" independently for completion, but increment one counter.
|
||||
// Logic: Both sides must complete the rep cycle? Or just one?
|
||||
// Python logic: "if self.raw_right_phase == 'done' and self.raw_left_phase == 'done'" -> REQUIRES BOTH.
|
||||
|
||||
const update_phase = (current_phase: string, stage: string | null) => {
|
||||
if (phase_type === 'start_down') {
|
||||
// Idle -> Down (Start) -> Up (Peak) -> Down (Done)
|
||||
// Note: "Down" is the REST state. "Up" is the ACTIVE state.
|
||||
// If we are IDLE, we wait for DOWN (Prep).
|
||||
// Actually, usually you start "Down".
|
||||
if (current_phase === 'idle' && stage === 'down') return 'down_prep';
|
||||
if (current_phase === 'down_prep' && stage === 'up') return 'up';
|
||||
if (current_phase === 'up' && stage === 'down') return 'done';
|
||||
} else {
|
||||
// Start Up (Squat).
|
||||
// Idle -> Up (Start) -> Down (Peak) -> Up (Done).
|
||||
if (current_phase === 'idle' && stage === 'up') return 'up_prep';
|
||||
if (current_phase === 'up_prep' && stage === 'down') return 'down'; // Peak
|
||||
if (current_phase === 'down' && stage === 'up') return 'done';
|
||||
}
|
||||
return current_phase;
|
||||
};
|
||||
|
||||
this.raw_right_phase = update_phase(this.raw_right_phase, this.stage_right) as any;
|
||||
this.raw_left_phase = update_phase(this.raw_left_phase, this.stage_left) as any;
|
||||
|
||||
// Completion Check
|
||||
// If bilateral (trackBothSides), wait for both.
|
||||
// We will assume bilateral for now as Python did.
|
||||
if (this.raw_right_phase === "done" && this.raw_left_phase === "done") {
|
||||
this.raw_reps[this.current_exercise] = (this.raw_reps[this.current_exercise] || 0) + 1;
|
||||
completed = true;
|
||||
|
||||
this.last_score = this.calculate_repetition_score(thresholds, dynamic_thresholds, this.current_exercise);
|
||||
|
||||
// FPS
|
||||
const fps = this.rep_data.frame_times.length > 0
|
||||
? 1000 / (this.rep_data.frame_times.reduce((a,b)=>a+b,0) / this.rep_data.frame_times.length || 1)
|
||||
: 0;
|
||||
|
||||
this.all_scores.push({ exercise: this.current_exercise, scores: this.last_score });
|
||||
|
||||
rep_summary = { scores: this.last_score, feedback: "Rep Completed", fps: fps, count: this.raw_reps[this.current_exercise] };
|
||||
this.rep_data = this._reset_rep_data();
|
||||
|
||||
this.raw_right_phase = "idle";
|
||||
this.raw_left_phase = "idle";
|
||||
}
|
||||
|
||||
return [this.stage_right, this.stage_left, completed, rep_summary];
|
||||
}
|
||||
}
|
||||
163
lib/pose/XGBoostPredictor.ts
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
|
||||
import modelData from '@/public/models/xgb_activity_model.json';
|
||||
|
||||
type TreeNode = {
|
||||
split_indices?: number;
|
||||
split_conditions?: number;
|
||||
yes?: number;
|
||||
no?: number;
|
||||
missing?: number;
|
||||
split_type?: number;
|
||||
leaf?: number;
|
||||
children?: TreeNode[];
|
||||
};
|
||||
|
||||
// The raw JSON structure from XGBoost dump
|
||||
interface XGBModelDump {
|
||||
learner: {
|
||||
gradient_booster: {
|
||||
model: {
|
||||
trees: Array<{
|
||||
base_weights: number[];
|
||||
default_left: number[];
|
||||
id: number;
|
||||
left_children: number[];
|
||||
loss_changes: number[];
|
||||
parents: number[];
|
||||
right_children: number[];
|
||||
split_conditions: number[];
|
||||
split_indices: number[];
|
||||
split_type: number[];
|
||||
sum_hessian: number[];
|
||||
tree_param: {
|
||||
num_deleted: string;
|
||||
num_feature: string;
|
||||
num_nodes: string;
|
||||
size_leaf_vector: string;
|
||||
};
|
||||
}>;
|
||||
gbtree_model_param: {
|
||||
num_parallel_tree: string;
|
||||
num_trees: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
learner_model_param: {
|
||||
base_score: string; // e.g. "[0.2, 0.3, 0.5]" or single float
|
||||
num_class: string;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export class XGBoostPredictor {
|
||||
private model: XGBModelDump;
|
||||
private numTrees: number;
|
||||
private numClass: number;
|
||||
private baseScores: number[];
|
||||
|
||||
constructor() {
|
||||
this.model = modelData as unknown as XGBModelDump;
|
||||
this.numTrees = parseInt(this.model.learner.gradient_booster.model.gbtree_model_param.num_trees);
|
||||
this.numClass = parseInt(this.model.learner.learner_model_param.num_class);
|
||||
|
||||
// Parse base score (often represented as an array string or a single float string)
|
||||
const baseScoreRaw = this.model.learner.learner_model_param.base_score;
|
||||
if (baseScoreRaw.startsWith('[')) {
|
||||
try {
|
||||
this.baseScores = JSON.parse(baseScoreRaw);
|
||||
} catch (e) {
|
||||
// Fallback manually parsing if JSON.parse fails on some formats
|
||||
console.error("Error parsing base_score", e);
|
||||
this.baseScores = Array(this.numClass).fill(0.5);
|
||||
}
|
||||
} else {
|
||||
this.baseScores = Array(this.numClass).fill(parseFloat(baseScoreRaw));
|
||||
}
|
||||
}
|
||||
|
||||
public predict(features: number[]): number[] {
|
||||
// Initialize scores with base_margin (inverse link of base_score usually, but for XGBoost multi-class
|
||||
// with 'multi:softprob', it usually starts at 0.5 before the tree sums if using raw margin,
|
||||
// but let's assume we sum the raw tree outputs).
|
||||
// Actually, XGBoost stores the raw margins.
|
||||
|
||||
const rawScores = new Array(this.numClass).fill(0.5);
|
||||
// NOTE: In strict XGBoost implementation, the initial prediction is 0.5 (logit)
|
||||
// if base_score is 0.5. For accurate results, we should check `base_score` parameter.
|
||||
// If base_scores are provided, we should convert them to margins if boosting starts from them.
|
||||
// Usually, sum = base_margin + sum(tree_outputs)
|
||||
|
||||
// Convert base scores to margins (logit)
|
||||
// margin = ln(p / (1-p)) is for binary. For multiclass, it's more complex.
|
||||
// Let's rely on standard additive behavior: rawScores starts at 0?
|
||||
// Or starts at the initial margin.
|
||||
|
||||
// Let's use 0.0 effectively and rely on Trees
|
||||
// (This might require tuning, but standard dump execution typically sums weights)
|
||||
const treeScores = new Array(this.numClass).fill(0);
|
||||
|
||||
const trees = this.model.learner.gradient_booster.model.trees;
|
||||
|
||||
for (let i = 0; i < this.numTrees; i++) {
|
||||
const tree = trees[i];
|
||||
const classIdx = i % this.numClass; // Trees are interleaved for classes 0, 1, 2, 0, 1, 2...
|
||||
|
||||
let nodeId = 0; // Start at root
|
||||
|
||||
// Traverse
|
||||
while (true) {
|
||||
// Check if leaf
|
||||
// In this JSON format, children arrays contain -1 for no child.
|
||||
// But we must check if the current node is a split or leaf.
|
||||
// The arrays (split_indices, etc.) are indexed by node ID.
|
||||
// Wait, the JSON format provided is aggressive: "left_children", "right_children" are arrays.
|
||||
|
||||
const leftChild = tree.left_children[nodeId];
|
||||
const rightChild = tree.right_children[nodeId];
|
||||
|
||||
// If leaf, left child is usually -1 (or similar indicator)
|
||||
// However, look at the values.
|
||||
// If index is valid split, proceed.
|
||||
|
||||
if (leftChild === -1 && rightChild === -1) {
|
||||
// Leaf node
|
||||
// Weight is in base_weights[nodeId]
|
||||
treeScores[classIdx] += tree.base_weights[nodeId];
|
||||
break;
|
||||
}
|
||||
|
||||
// Split
|
||||
const featureIdx = tree.split_indices[nodeId];
|
||||
const threshold = tree.split_conditions[nodeId];
|
||||
const defaultLeft = tree.default_left[nodeId] === 1;
|
||||
|
||||
const featureVal = features[featureIdx];
|
||||
|
||||
// Missing value handling (if feature is NaN, go default)
|
||||
if (featureVal === undefined || isNaN(featureVal)) {
|
||||
nodeId = defaultLeft ? leftChild : rightChild;
|
||||
} else {
|
||||
if (featureVal < threshold) {
|
||||
nodeId = leftChild;
|
||||
} else {
|
||||
nodeId = rightChild;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Softmax
|
||||
// First add base margin?
|
||||
// For 'multi:softprob', output is softmax(raw_score + base_margin)
|
||||
// If base_score=[0.5, 0.5, 0.5], base_margin ~ 0.
|
||||
|
||||
return this.softmax(treeScores);
|
||||
}
|
||||
|
||||
private softmax(logits: number[]): number[] {
|
||||
const maxLogit = Math.max(...logits);
|
||||
const scores = logits.map(l => Math.exp(l - maxLogit));
|
||||
const sumScores = scores.reduce((a, b) => a + b, 0);
|
||||
return scores.map(s => s / sumScores);
|
||||
}
|
||||
}
|
||||
34
lib/prisma-gen/browser.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
// @ts-nocheck
|
||||
/*
|
||||
* This file should be your main import to use Prisma-related types and utilities in a browser.
|
||||
* Use it to get access to models, enums, and input types.
|
||||
*
|
||||
* This file does not contain a `PrismaClient` class, nor several other helpers that are intended as server-side only.
|
||||
* See `client.ts` for the standard, server-side entry point.
|
||||
*
|
||||
* 🟢 You can import this file directly.
|
||||
*/
|
||||
|
||||
import * as Prisma from './internal/prismaNamespaceBrowser.js'
|
||||
export { Prisma }
|
||||
export * as $Enums from './enums.js'
|
||||
export * from './enums.js';
|
||||
/**
|
||||
* Model activity_logs
|
||||
*
|
||||
*/
|
||||
export type activity_logs = Prisma.activity_logsModel
|
||||
/**
|
||||
* Model training_menus
|
||||
*
|
||||
*/
|
||||
export type training_menus = Prisma.training_menusModel
|
||||
/**
|
||||
* Model user_recaps
|
||||
*
|
||||
*/
|
||||
export type user_recaps = Prisma.user_recapsModel
|
||||
56
lib/prisma-gen/client.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
// @ts-nocheck
|
||||
/*
|
||||
* This file should be your main import to use Prisma. Through it you get access to all the models, enums, and input types.
|
||||
* If you're looking for something you can import in the client-side of your application, please refer to the `browser.ts` file instead.
|
||||
*
|
||||
* 🟢 You can import this file directly.
|
||||
*/
|
||||
|
||||
import * as process from 'node:process'
|
||||
import * as path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
globalThis['__dirname'] = path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
import * as runtime from "@prisma/client/runtime/client"
|
||||
import * as $Enums from "./enums.js"
|
||||
import * as $Class from "./internal/class.js"
|
||||
import * as Prisma from "./internal/prismaNamespace.js"
|
||||
|
||||
export * as $Enums from './enums.js'
|
||||
export * from "./enums.js"
|
||||
/**
|
||||
* ## Prisma Client
|
||||
*
|
||||
* Type-safe database client for TypeScript
|
||||
* @example
|
||||
* ```
|
||||
* const prisma = new PrismaClient()
|
||||
* // Fetch zero or more Activity_logs
|
||||
* const activity_logs = await prisma.activity_logs.findMany()
|
||||
* ```
|
||||
*
|
||||
* Read more in our [docs](https://pris.ly/d/client).
|
||||
*/
|
||||
export const PrismaClient = $Class.getPrismaClientClass()
|
||||
export type PrismaClient<LogOpts extends Prisma.LogLevel = never, OmitOpts extends Prisma.PrismaClientOptions["omit"] = Prisma.PrismaClientOptions["omit"], ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = $Class.PrismaClient<LogOpts, OmitOpts, ExtArgs>
|
||||
export { Prisma }
|
||||
|
||||
/**
|
||||
* Model activity_logs
|
||||
*
|
||||
*/
|
||||
export type activity_logs = Prisma.activity_logsModel
|
||||
/**
|
||||
* Model training_menus
|
||||
*
|
||||
*/
|
||||
export type training_menus = Prisma.training_menusModel
|
||||
/**
|
||||
* Model user_recaps
|
||||
*
|
||||
*/
|
||||
export type user_recaps = Prisma.user_recapsModel
|
||||
341
lib/prisma-gen/commonInputTypes.ts
Normal file
|
|
@ -0,0 +1,341 @@
|
|||
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
// @ts-nocheck
|
||||
/*
|
||||
* This file exports various common sort, input & filter types that are not directly linked to a particular model.
|
||||
*
|
||||
* 🟢 You can import this file directly.
|
||||
*/
|
||||
|
||||
import type * as runtime from "@prisma/client/runtime/client"
|
||||
import * as $Enums from "./enums.js"
|
||||
import type * as Prisma from "./internal/prismaNamespace.js"
|
||||
|
||||
|
||||
export type IntFilter<$PrismaModel = never> = {
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
|
||||
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
|
||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedIntFilter<$PrismaModel> | number
|
||||
}
|
||||
|
||||
export type DateTimeNullableFilter<$PrismaModel = never> = {
|
||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
|
||||
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
|
||||
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
|
||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> | Date | string | null
|
||||
}
|
||||
|
||||
export type StringNullableFilter<$PrismaModel = never> = {
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
||||
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
|
||||
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
mode?: Prisma.QueryMode
|
||||
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null
|
||||
}
|
||||
|
||||
export type JsonNullableFilter<$PrismaModel = never> =
|
||||
| Prisma.PatchUndefined<
|
||||
Prisma.Either<Required<JsonNullableFilterBase<$PrismaModel>>, Exclude<keyof Required<JsonNullableFilterBase<$PrismaModel>>, 'path'>>,
|
||||
Required<JsonNullableFilterBase<$PrismaModel>>
|
||||
>
|
||||
| Prisma.OptionalFlat<Omit<Required<JsonNullableFilterBase<$PrismaModel>>, 'path'>>
|
||||
|
||||
export type JsonNullableFilterBase<$PrismaModel = never> = {
|
||||
equals?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
|
||||
path?: string[]
|
||||
mode?: Prisma.QueryMode | Prisma.EnumQueryModeFieldRefInput<$PrismaModel>
|
||||
string_contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
string_starts_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
string_ends_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
array_starts_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
|
||||
array_ends_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
|
||||
array_contains?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
|
||||
lt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
|
||||
lte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
|
||||
gt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
|
||||
gte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
|
||||
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
|
||||
}
|
||||
|
||||
export type SortOrderInput = {
|
||||
sort: Prisma.SortOrder
|
||||
nulls?: Prisma.NullsOrder
|
||||
}
|
||||
|
||||
export type IntWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
|
||||
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
|
||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedIntWithAggregatesFilter<$PrismaModel> | number
|
||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_avg?: Prisma.NestedFloatFilter<$PrismaModel>
|
||||
_sum?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type DateTimeNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
|
||||
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
|
||||
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
|
||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedDateTimeNullableWithAggregatesFilter<$PrismaModel> | Date | string | null
|
||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type StringNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
||||
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
|
||||
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
mode?: Prisma.QueryMode
|
||||
not?: Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel> | string | null
|
||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type JsonNullableWithAggregatesFilter<$PrismaModel = never> =
|
||||
| Prisma.PatchUndefined<
|
||||
Prisma.Either<Required<JsonNullableWithAggregatesFilterBase<$PrismaModel>>, Exclude<keyof Required<JsonNullableWithAggregatesFilterBase<$PrismaModel>>, 'path'>>,
|
||||
Required<JsonNullableWithAggregatesFilterBase<$PrismaModel>>
|
||||
>
|
||||
| Prisma.OptionalFlat<Omit<Required<JsonNullableWithAggregatesFilterBase<$PrismaModel>>, 'path'>>
|
||||
|
||||
export type JsonNullableWithAggregatesFilterBase<$PrismaModel = never> = {
|
||||
equals?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
|
||||
path?: string[]
|
||||
mode?: Prisma.QueryMode | Prisma.EnumQueryModeFieldRefInput<$PrismaModel>
|
||||
string_contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
string_starts_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
string_ends_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
array_starts_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
|
||||
array_ends_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
|
||||
array_contains?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
|
||||
lt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
|
||||
lte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
|
||||
gt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
|
||||
gte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
|
||||
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
|
||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedJsonNullableFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedJsonNullableFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type IntNullableFilter<$PrismaModel = never> = {
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
|
||||
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
|
||||
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
|
||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedIntNullableFilter<$PrismaModel> | number | null
|
||||
}
|
||||
|
||||
export type IntNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
|
||||
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
|
||||
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
|
||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedIntNullableWithAggregatesFilter<$PrismaModel> | number | null
|
||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
_avg?: Prisma.NestedFloatNullableFilter<$PrismaModel>
|
||||
_sum?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type NestedIntFilter<$PrismaModel = never> = {
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
|
||||
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
|
||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedIntFilter<$PrismaModel> | number
|
||||
}
|
||||
|
||||
export type NestedDateTimeNullableFilter<$PrismaModel = never> = {
|
||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
|
||||
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
|
||||
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
|
||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> | Date | string | null
|
||||
}
|
||||
|
||||
export type NestedStringNullableFilter<$PrismaModel = never> = {
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
||||
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
|
||||
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null
|
||||
}
|
||||
|
||||
export type NestedIntWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
|
||||
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
|
||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedIntWithAggregatesFilter<$PrismaModel> | number
|
||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_avg?: Prisma.NestedFloatFilter<$PrismaModel>
|
||||
_sum?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type NestedFloatFilter<$PrismaModel = never> = {
|
||||
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||
in?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel>
|
||||
notIn?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel>
|
||||
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedFloatFilter<$PrismaModel> | number
|
||||
}
|
||||
|
||||
export type NestedDateTimeNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
|
||||
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
|
||||
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
|
||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedDateTimeNullableWithAggregatesFilter<$PrismaModel> | Date | string | null
|
||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type NestedIntNullableFilter<$PrismaModel = never> = {
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
|
||||
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
|
||||
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
|
||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedIntNullableFilter<$PrismaModel> | number | null
|
||||
}
|
||||
|
||||
export type NestedStringNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
||||
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
|
||||
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel> | string | null
|
||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type NestedJsonNullableFilter<$PrismaModel = never> =
|
||||
| Prisma.PatchUndefined<
|
||||
Prisma.Either<Required<NestedJsonNullableFilterBase<$PrismaModel>>, Exclude<keyof Required<NestedJsonNullableFilterBase<$PrismaModel>>, 'path'>>,
|
||||
Required<NestedJsonNullableFilterBase<$PrismaModel>>
|
||||
>
|
||||
| Prisma.OptionalFlat<Omit<Required<NestedJsonNullableFilterBase<$PrismaModel>>, 'path'>>
|
||||
|
||||
export type NestedJsonNullableFilterBase<$PrismaModel = never> = {
|
||||
equals?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
|
||||
path?: string[]
|
||||
mode?: Prisma.QueryMode | Prisma.EnumQueryModeFieldRefInput<$PrismaModel>
|
||||
string_contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
string_starts_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
string_ends_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
array_starts_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
|
||||
array_ends_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
|
||||
array_contains?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
|
||||
lt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
|
||||
lte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
|
||||
gt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
|
||||
gte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
|
||||
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
|
||||
}
|
||||
|
||||
export type NestedIntNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
|
||||
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
|
||||
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
|
||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedIntNullableWithAggregatesFilter<$PrismaModel> | number | null
|
||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
_avg?: Prisma.NestedFloatNullableFilter<$PrismaModel>
|
||||
_sum?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type NestedFloatNullableFilter<$PrismaModel = never> = {
|
||||
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel> | null
|
||||
in?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> | null
|
||||
notIn?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> | null
|
||||
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedFloatNullableFilter<$PrismaModel> | number | null
|
||||
}
|
||||
|
||||
|
||||
15
lib/prisma-gen/enums.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
// @ts-nocheck
|
||||
/*
|
||||
* This file exports all enum related types from the schema.
|
||||
*
|
||||
* 🟢 You can import this file directly.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
// This file is empty because there are no enums in the schema.
|
||||
export {}
|
||||
210
lib/prisma-gen/internal/class.ts
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
// @ts-nocheck
|
||||
/*
|
||||
* WARNING: This is an internal file that is subject to change!
|
||||
*
|
||||
* 🛑 Under no circumstances should you import this file directly! 🛑
|
||||
*
|
||||
* Please import the `PrismaClient` class from the `client.ts` file instead.
|
||||
*/
|
||||
|
||||
import * as runtime from "@prisma/client/runtime/client"
|
||||
import type * as Prisma from "./prismaNamespace.js"
|
||||
|
||||
|
||||
const config: runtime.GetPrismaClientConfig = {
|
||||
"previewFeatures": [],
|
||||
"clientVersion": "7.2.0",
|
||||
"engineVersion": "0c8ef2ce45c83248ab3df073180d5eda9e8be7a3",
|
||||
"activeProvider": "postgresql",
|
||||
"inlineSchema": "generator client {\n provider = \"prisma-client\"\n output = \"../lib/prisma-gen\"\n engineType = \"binary\"\n}\n\ndatasource db {\n provider = \"postgresql\"\n}\n\nmodel activity_logs {\n id Int @id @default(autoincrement())\n timestamp DateTime? @db.Timestamp(6)\n status String? @db.VarChar\n confidence String? @db.VarChar\n details Json?\n\n @@index([id], map: \"ix_activity_logs_id\")\n}\n\nmodel training_menus {\n id Int @id @default(autoincrement())\n name String? @db.VarChar\n exercises Json?\n created_at DateTime? @db.Timestamp(6)\n user_recaps user_recaps[]\n\n @@index([id], map: \"ix_training_menus_id\")\n @@index([name], map: \"ix_training_menus_name\")\n}\n\nmodel user_recaps {\n id Int @id @default(autoincrement())\n menu_id Int?\n summary Json?\n completed_at DateTime? @db.Timestamp(6)\n training_menus training_menus? @relation(fields: [menu_id], references: [id], onDelete: NoAction, onUpdate: NoAction)\n\n @@index([id], map: \"ix_user_recaps_id\")\n}\n",
|
||||
"runtimeDataModel": {
|
||||
"models": {},
|
||||
"enums": {},
|
||||
"types": {}
|
||||
}
|
||||
}
|
||||
|
||||
config.runtimeDataModel = JSON.parse("{\"models\":{\"activity_logs\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"timestamp\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"status\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"confidence\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"details\",\"kind\":\"scalar\",\"type\":\"Json\"}],\"dbName\":null},\"training_menus\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"exercises\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"created_at\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"user_recaps\",\"kind\":\"object\",\"type\":\"user_recaps\",\"relationName\":\"training_menusTouser_recaps\"}],\"dbName\":null},\"user_recaps\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"menu_id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"summary\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"completed_at\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"training_menus\",\"kind\":\"object\",\"type\":\"training_menus\",\"relationName\":\"training_menusTouser_recaps\"}],\"dbName\":null}},\"enums\":{},\"types\":{}}")
|
||||
|
||||
async function decodeBase64AsWasm(wasmBase64: string): Promise<WebAssembly.Module> {
|
||||
const { Buffer } = await import('node:buffer')
|
||||
const wasmArray = Buffer.from(wasmBase64, 'base64')
|
||||
return new WebAssembly.Module(wasmArray)
|
||||
}
|
||||
|
||||
config.compilerWasm = {
|
||||
getRuntime: async () => await import("@prisma/client/runtime/query_compiler_bg.postgresql.mjs"),
|
||||
|
||||
getQueryCompilerWasmModule: async () => {
|
||||
const { wasm } = await import("@prisma/client/runtime/query_compiler_bg.postgresql.wasm-base64.mjs")
|
||||
return await decodeBase64AsWasm(wasm)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
export type LogOptions<ClientOptions extends Prisma.PrismaClientOptions> =
|
||||
'log' extends keyof ClientOptions ? ClientOptions['log'] extends Array<Prisma.LogLevel | Prisma.LogDefinition> ? Prisma.GetEvents<ClientOptions['log']> : never : never
|
||||
|
||||
export interface PrismaClientConstructor {
|
||||
/**
|
||||
* ## Prisma Client
|
||||
*
|
||||
* Type-safe database client for TypeScript
|
||||
* @example
|
||||
* ```
|
||||
* const prisma = new PrismaClient()
|
||||
* // Fetch zero or more Activity_logs
|
||||
* const activity_logs = await prisma.activity_logs.findMany()
|
||||
* ```
|
||||
*
|
||||
* Read more in our [docs](https://pris.ly/d/client).
|
||||
*/
|
||||
|
||||
new <
|
||||
Options extends Prisma.PrismaClientOptions = Prisma.PrismaClientOptions,
|
||||
LogOpts extends LogOptions<Options> = LogOptions<Options>,
|
||||
OmitOpts extends Prisma.PrismaClientOptions['omit'] = Options extends { omit: infer U } ? U : Prisma.PrismaClientOptions['omit'],
|
||||
ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs
|
||||
>(options: Prisma.Subset<Options, Prisma.PrismaClientOptions> ): PrismaClient<LogOpts, OmitOpts, ExtArgs>
|
||||
}
|
||||
|
||||
/**
|
||||
* ## Prisma Client
|
||||
*
|
||||
* Type-safe database client for TypeScript
|
||||
* @example
|
||||
* ```
|
||||
* const prisma = new PrismaClient()
|
||||
* // Fetch zero or more Activity_logs
|
||||
* const activity_logs = await prisma.activity_logs.findMany()
|
||||
* ```
|
||||
*
|
||||
* Read more in our [docs](https://pris.ly/d/client).
|
||||
*/
|
||||
|
||||
export interface PrismaClient<
|
||||
in LogOpts extends Prisma.LogLevel = never,
|
||||
in out OmitOpts extends Prisma.PrismaClientOptions['omit'] = undefined,
|
||||
in out ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs
|
||||
> {
|
||||
[K: symbol]: { types: Prisma.TypeMap<ExtArgs>['other'] }
|
||||
|
||||
$on<V extends LogOpts>(eventType: V, callback: (event: V extends 'query' ? Prisma.QueryEvent : Prisma.LogEvent) => void): PrismaClient;
|
||||
|
||||
/**
|
||||
* Connect with the database
|
||||
*/
|
||||
$connect(): runtime.Types.Utils.JsPromise<void>;
|
||||
|
||||
/**
|
||||
* Disconnect from the database
|
||||
*/
|
||||
$disconnect(): runtime.Types.Utils.JsPromise<void>;
|
||||
|
||||
/**
|
||||
* Executes a prepared raw query and returns the number of affected rows.
|
||||
* @example
|
||||
* ```
|
||||
* const result = await prisma.$executeRaw`UPDATE User SET cool = ${true} WHERE email = ${'user@email.com'};`
|
||||
* ```
|
||||
*
|
||||
* Read more in our [docs](https://pris.ly/d/raw-queries).
|
||||
*/
|
||||
$executeRaw<T = unknown>(query: TemplateStringsArray | Prisma.Sql, ...values: any[]): Prisma.PrismaPromise<number>;
|
||||
|
||||
/**
|
||||
* Executes a raw query and returns the number of affected rows.
|
||||
* Susceptible to SQL injections, see documentation.
|
||||
* @example
|
||||
* ```
|
||||
* const result = await prisma.$executeRawUnsafe('UPDATE User SET cool = $1 WHERE email = $2 ;', true, 'user@email.com')
|
||||
* ```
|
||||
*
|
||||
* Read more in our [docs](https://pris.ly/d/raw-queries).
|
||||
*/
|
||||
$executeRawUnsafe<T = unknown>(query: string, ...values: any[]): Prisma.PrismaPromise<number>;
|
||||
|
||||
/**
|
||||
* Performs a prepared raw query and returns the `SELECT` data.
|
||||
* @example
|
||||
* ```
|
||||
* const result = await prisma.$queryRaw`SELECT * FROM User WHERE id = ${1} OR email = ${'user@email.com'};`
|
||||
* ```
|
||||
*
|
||||
* Read more in our [docs](https://pris.ly/d/raw-queries).
|
||||
*/
|
||||
$queryRaw<T = unknown>(query: TemplateStringsArray | Prisma.Sql, ...values: any[]): Prisma.PrismaPromise<T>;
|
||||
|
||||
/**
|
||||
* Performs a raw query and returns the `SELECT` data.
|
||||
* Susceptible to SQL injections, see documentation.
|
||||
* @example
|
||||
* ```
|
||||
* const result = await prisma.$queryRawUnsafe('SELECT * FROM User WHERE id = $1 OR email = $2;', 1, 'user@email.com')
|
||||
* ```
|
||||
*
|
||||
* Read more in our [docs](https://pris.ly/d/raw-queries).
|
||||
*/
|
||||
$queryRawUnsafe<T = unknown>(query: string, ...values: any[]): Prisma.PrismaPromise<T>;
|
||||
|
||||
|
||||
/**
|
||||
* Allows the running of a sequence of read/write operations that are guaranteed to either succeed or fail as a whole.
|
||||
* @example
|
||||
* ```
|
||||
* const [george, bob, alice] = await prisma.$transaction([
|
||||
* prisma.user.create({ data: { name: 'George' } }),
|
||||
* prisma.user.create({ data: { name: 'Bob' } }),
|
||||
* prisma.user.create({ data: { name: 'Alice' } }),
|
||||
* ])
|
||||
* ```
|
||||
*
|
||||
* Read more in our [docs](https://www.prisma.io/docs/concepts/components/prisma-client/transactions).
|
||||
*/
|
||||
$transaction<P extends Prisma.PrismaPromise<any>[]>(arg: [...P], options?: { isolationLevel?: Prisma.TransactionIsolationLevel }): runtime.Types.Utils.JsPromise<runtime.Types.Utils.UnwrapTuple<P>>
|
||||
|
||||
$transaction<R>(fn: (prisma: Omit<PrismaClient, runtime.ITXClientDenyList>) => runtime.Types.Utils.JsPromise<R>, options?: { maxWait?: number, timeout?: number, isolationLevel?: Prisma.TransactionIsolationLevel }): runtime.Types.Utils.JsPromise<R>
|
||||
|
||||
$extends: runtime.Types.Extensions.ExtendsHook<"extends", Prisma.TypeMapCb<OmitOpts>, ExtArgs, runtime.Types.Utils.Call<Prisma.TypeMapCb<OmitOpts>, {
|
||||
extArgs: ExtArgs
|
||||
}>>
|
||||
|
||||
/**
|
||||
* `prisma.activity_logs`: Exposes CRUD operations for the **activity_logs** model.
|
||||
* Example usage:
|
||||
* ```ts
|
||||
* // Fetch zero or more Activity_logs
|
||||
* const activity_logs = await prisma.activity_logs.findMany()
|
||||
* ```
|
||||
*/
|
||||
get activity_logs(): Prisma.activity_logsDelegate<ExtArgs, { omit: OmitOpts }>;
|
||||
|
||||
/**
|
||||
* `prisma.training_menus`: Exposes CRUD operations for the **training_menus** model.
|
||||
* Example usage:
|
||||
* ```ts
|
||||
* // Fetch zero or more Training_menus
|
||||
* const training_menus = await prisma.training_menus.findMany()
|
||||
* ```
|
||||
*/
|
||||
get training_menus(): Prisma.training_menusDelegate<ExtArgs, { omit: OmitOpts }>;
|
||||
|
||||
/**
|
||||
* `prisma.user_recaps`: Exposes CRUD operations for the **user_recaps** model.
|
||||
* Example usage:
|
||||
* ```ts
|
||||
* // Fetch zero or more User_recaps
|
||||
* const user_recaps = await prisma.user_recaps.findMany()
|
||||
* ```
|
||||
*/
|
||||
get user_recaps(): Prisma.user_recapsDelegate<ExtArgs, { omit: OmitOpts }>;
|
||||
}
|
||||
|
||||
export function getPrismaClientClass(): PrismaClientConstructor {
|
||||
return runtime.getPrismaClient(config) as unknown as PrismaClientConstructor
|
||||
}
|
||||
977
lib/prisma-gen/internal/prismaNamespace.ts
Normal file
|
|
@ -0,0 +1,977 @@
|
|||
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
// @ts-nocheck
|
||||
/*
|
||||
* WARNING: This is an internal file that is subject to change!
|
||||
*
|
||||
* 🛑 Under no circumstances should you import this file directly! 🛑
|
||||
*
|
||||
* All exports from this file are wrapped under a `Prisma` namespace object in the client.ts file.
|
||||
* While this enables partial backward compatibility, it is not part of the stable public API.
|
||||
*
|
||||
* If you are looking for your Models, Enums, and Input Types, please import them from the respective
|
||||
* model files in the `model` directory!
|
||||
*/
|
||||
|
||||
import * as runtime from "@prisma/client/runtime/client"
|
||||
import type * as Prisma from "../models.js"
|
||||
import { type PrismaClient } from "./class.js"
|
||||
|
||||
export type * from '../models.js'
|
||||
|
||||
export type DMMF = typeof runtime.DMMF
|
||||
|
||||
export type PrismaPromise<T> = runtime.Types.Public.PrismaPromise<T>
|
||||
|
||||
/**
|
||||
* Prisma Errors
|
||||
*/
|
||||
|
||||
export const PrismaClientKnownRequestError = runtime.PrismaClientKnownRequestError
|
||||
export type PrismaClientKnownRequestError = runtime.PrismaClientKnownRequestError
|
||||
|
||||
export const PrismaClientUnknownRequestError = runtime.PrismaClientUnknownRequestError
|
||||
export type PrismaClientUnknownRequestError = runtime.PrismaClientUnknownRequestError
|
||||
|
||||
export const PrismaClientRustPanicError = runtime.PrismaClientRustPanicError
|
||||
export type PrismaClientRustPanicError = runtime.PrismaClientRustPanicError
|
||||
|
||||
export const PrismaClientInitializationError = runtime.PrismaClientInitializationError
|
||||
export type PrismaClientInitializationError = runtime.PrismaClientInitializationError
|
||||
|
||||
export const PrismaClientValidationError = runtime.PrismaClientValidationError
|
||||
export type PrismaClientValidationError = runtime.PrismaClientValidationError
|
||||
|
||||
/**
|
||||
* Re-export of sql-template-tag
|
||||
*/
|
||||
export const sql = runtime.sqltag
|
||||
export const empty = runtime.empty
|
||||
export const join = runtime.join
|
||||
export const raw = runtime.raw
|
||||
export const Sql = runtime.Sql
|
||||
export type Sql = runtime.Sql
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Decimal.js
|
||||
*/
|
||||
export const Decimal = runtime.Decimal
|
||||
export type Decimal = runtime.Decimal
|
||||
|
||||
export type DecimalJsLike = runtime.DecimalJsLike
|
||||
|
||||
/**
|
||||
* Extensions
|
||||
*/
|
||||
export type Extension = runtime.Types.Extensions.UserArgs
|
||||
export const getExtensionContext = runtime.Extensions.getExtensionContext
|
||||
export type Args<T, F extends runtime.Operation> = runtime.Types.Public.Args<T, F>
|
||||
export type Payload<T, F extends runtime.Operation = never> = runtime.Types.Public.Payload<T, F>
|
||||
export type Result<T, A, F extends runtime.Operation> = runtime.Types.Public.Result<T, A, F>
|
||||
export type Exact<A, W> = runtime.Types.Public.Exact<A, W>
|
||||
|
||||
export type PrismaVersion = {
|
||||
client: string
|
||||
engine: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Prisma Client JS version: 7.2.0
|
||||
* Query Engine version: 0c8ef2ce45c83248ab3df073180d5eda9e8be7a3
|
||||
*/
|
||||
export const prismaVersion: PrismaVersion = {
|
||||
client: "7.2.0",
|
||||
engine: "0c8ef2ce45c83248ab3df073180d5eda9e8be7a3"
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility Types
|
||||
*/
|
||||
|
||||
export type Bytes = runtime.Bytes
|
||||
export type JsonObject = runtime.JsonObject
|
||||
export type JsonArray = runtime.JsonArray
|
||||
export type JsonValue = runtime.JsonValue
|
||||
export type InputJsonObject = runtime.InputJsonObject
|
||||
export type InputJsonArray = runtime.InputJsonArray
|
||||
export type InputJsonValue = runtime.InputJsonValue
|
||||
|
||||
|
||||
export const NullTypes = {
|
||||
DbNull: runtime.NullTypes.DbNull as (new (secret: never) => typeof runtime.DbNull),
|
||||
JsonNull: runtime.NullTypes.JsonNull as (new (secret: never) => typeof runtime.JsonNull),
|
||||
AnyNull: runtime.NullTypes.AnyNull as (new (secret: never) => typeof runtime.AnyNull),
|
||||
}
|
||||
/**
|
||||
* Helper for filtering JSON entries that have `null` on the database (empty on the db)
|
||||
*
|
||||
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
|
||||
*/
|
||||
export const DbNull = runtime.DbNull
|
||||
|
||||
/**
|
||||
* Helper for filtering JSON entries that have JSON `null` values (not empty on the db)
|
||||
*
|
||||
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
|
||||
*/
|
||||
export const JsonNull = runtime.JsonNull
|
||||
|
||||
/**
|
||||
* Helper for filtering JSON entries that are `Prisma.DbNull` or `Prisma.JsonNull`
|
||||
*
|
||||
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
|
||||
*/
|
||||
export const AnyNull = runtime.AnyNull
|
||||
|
||||
|
||||
type SelectAndInclude = {
|
||||
select: any
|
||||
include: any
|
||||
}
|
||||
|
||||
type SelectAndOmit = {
|
||||
select: any
|
||||
omit: any
|
||||
}
|
||||
|
||||
/**
|
||||
* From T, pick a set of properties whose keys are in the union K
|
||||
*/
|
||||
type Prisma__Pick<T, K extends keyof T> = {
|
||||
[P in K]: T[P];
|
||||
};
|
||||
|
||||
export type Enumerable<T> = T | Array<T>;
|
||||
|
||||
/**
|
||||
* Subset
|
||||
* @desc From `T` pick properties that exist in `U`. Simple version of Intersection
|
||||
*/
|
||||
export type Subset<T, U> = {
|
||||
[key in keyof T]: key extends keyof U ? T[key] : never;
|
||||
};
|
||||
|
||||
/**
|
||||
* SelectSubset
|
||||
* @desc From `T` pick properties that exist in `U`. Simple version of Intersection.
|
||||
* Additionally, it validates, if both select and include are present. If the case, it errors.
|
||||
*/
|
||||
export type SelectSubset<T, U> = {
|
||||
[key in keyof T]: key extends keyof U ? T[key] : never
|
||||
} &
|
||||
(T extends SelectAndInclude
|
||||
? 'Please either choose `select` or `include`.'
|
||||
: T extends SelectAndOmit
|
||||
? 'Please either choose `select` or `omit`.'
|
||||
: {})
|
||||
|
||||
/**
|
||||
* Subset + Intersection
|
||||
* @desc From `T` pick properties that exist in `U` and intersect `K`
|
||||
*/
|
||||
export type SubsetIntersection<T, U, K> = {
|
||||
[key in keyof T]: key extends keyof U ? T[key] : never
|
||||
} &
|
||||
K
|
||||
|
||||
type Without<T, U> = { [P in Exclude<keyof T, keyof U>]?: never };
|
||||
|
||||
/**
|
||||
* XOR is needed to have a real mutually exclusive union type
|
||||
* https://stackoverflow.com/questions/42123407/does-typescript-support-mutually-exclusive-types
|
||||
*/
|
||||
export type XOR<T, U> =
|
||||
T extends object ?
|
||||
U extends object ?
|
||||
(Without<T, U> & U) | (Without<U, T> & T)
|
||||
: U : T
|
||||
|
||||
|
||||
/**
|
||||
* Is T a Record?
|
||||
*/
|
||||
type IsObject<T extends any> = T extends Array<any>
|
||||
? False
|
||||
: T extends Date
|
||||
? False
|
||||
: T extends Uint8Array
|
||||
? False
|
||||
: T extends BigInt
|
||||
? False
|
||||
: T extends object
|
||||
? True
|
||||
: False
|
||||
|
||||
|
||||
/**
|
||||
* If it's T[], return T
|
||||
*/
|
||||
export type UnEnumerate<T extends unknown> = T extends Array<infer U> ? U : T
|
||||
|
||||
/**
|
||||
* From ts-toolbelt
|
||||
*/
|
||||
|
||||
type __Either<O extends object, K extends Key> = Omit<O, K> &
|
||||
{
|
||||
// Merge all but K
|
||||
[P in K]: Prisma__Pick<O, P & keyof O> // With K possibilities
|
||||
}[K]
|
||||
|
||||
type EitherStrict<O extends object, K extends Key> = Strict<__Either<O, K>>
|
||||
|
||||
type EitherLoose<O extends object, K extends Key> = ComputeRaw<__Either<O, K>>
|
||||
|
||||
type _Either<
|
||||
O extends object,
|
||||
K extends Key,
|
||||
strict extends Boolean
|
||||
> = {
|
||||
1: EitherStrict<O, K>
|
||||
0: EitherLoose<O, K>
|
||||
}[strict]
|
||||
|
||||
export type Either<
|
||||
O extends object,
|
||||
K extends Key,
|
||||
strict extends Boolean = 1
|
||||
> = O extends unknown ? _Either<O, K, strict> : never
|
||||
|
||||
export type Union = any
|
||||
|
||||
export type PatchUndefined<O extends object, O1 extends object> = {
|
||||
[K in keyof O]: O[K] extends undefined ? At<O1, K> : O[K]
|
||||
} & {}
|
||||
|
||||
/** Helper Types for "Merge" **/
|
||||
export type IntersectOf<U extends Union> = (
|
||||
U extends unknown ? (k: U) => void : never
|
||||
) extends (k: infer I) => void
|
||||
? I
|
||||
: never
|
||||
|
||||
export type Overwrite<O extends object, O1 extends object> = {
|
||||
[K in keyof O]: K extends keyof O1 ? O1[K] : O[K];
|
||||
} & {};
|
||||
|
||||
type _Merge<U extends object> = IntersectOf<Overwrite<U, {
|
||||
[K in keyof U]-?: At<U, K>;
|
||||
}>>;
|
||||
|
||||
type Key = string | number | symbol;
|
||||
type AtStrict<O extends object, K extends Key> = O[K & keyof O];
|
||||
type AtLoose<O extends object, K extends Key> = O extends unknown ? AtStrict<O, K> : never;
|
||||
export type At<O extends object, K extends Key, strict extends Boolean = 1> = {
|
||||
1: AtStrict<O, K>;
|
||||
0: AtLoose<O, K>;
|
||||
}[strict];
|
||||
|
||||
export type ComputeRaw<A extends any> = A extends Function ? A : {
|
||||
[K in keyof A]: A[K];
|
||||
} & {};
|
||||
|
||||
export type OptionalFlat<O> = {
|
||||
[K in keyof O]?: O[K];
|
||||
} & {};
|
||||
|
||||
type _Record<K extends keyof any, T> = {
|
||||
[P in K]: T;
|
||||
};
|
||||
|
||||
// cause typescript not to expand types and preserve names
|
||||
type NoExpand<T> = T extends unknown ? T : never;
|
||||
|
||||
// this type assumes the passed object is entirely optional
|
||||
export type AtLeast<O extends object, K extends string> = NoExpand<
|
||||
O extends unknown
|
||||
? | (K extends keyof O ? { [P in K]: O[P] } & O : O)
|
||||
| {[P in keyof O as P extends K ? P : never]-?: O[P]} & O
|
||||
: never>;
|
||||
|
||||
type _Strict<U, _U = U> = U extends unknown ? U & OptionalFlat<_Record<Exclude<Keys<_U>, keyof U>, never>> : never;
|
||||
|
||||
export type Strict<U extends object> = ComputeRaw<_Strict<U>>;
|
||||
/** End Helper Types for "Merge" **/
|
||||
|
||||
export type Merge<U extends object> = ComputeRaw<_Merge<Strict<U>>>;
|
||||
|
||||
export type Boolean = True | False
|
||||
|
||||
export type True = 1
|
||||
|
||||
export type False = 0
|
||||
|
||||
export type Not<B extends Boolean> = {
|
||||
0: 1
|
||||
1: 0
|
||||
}[B]
|
||||
|
||||
export type Extends<A1 extends any, A2 extends any> = [A1] extends [never]
|
||||
? 0 // anything `never` is false
|
||||
: A1 extends A2
|
||||
? 1
|
||||
: 0
|
||||
|
||||
export type Has<U extends Union, U1 extends Union> = Not<
|
||||
Extends<Exclude<U1, U>, U1>
|
||||
>
|
||||
|
||||
export type Or<B1 extends Boolean, B2 extends Boolean> = {
|
||||
0: {
|
||||
0: 0
|
||||
1: 1
|
||||
}
|
||||
1: {
|
||||
0: 1
|
||||
1: 1
|
||||
}
|
||||
}[B1][B2]
|
||||
|
||||
export type Keys<U extends Union> = U extends unknown ? keyof U : never
|
||||
|
||||
export type GetScalarType<T, O> = O extends object ? {
|
||||
[P in keyof T]: P extends keyof O
|
||||
? O[P]
|
||||
: never
|
||||
} : never
|
||||
|
||||
type FieldPaths<
|
||||
T,
|
||||
U = Omit<T, '_avg' | '_sum' | '_count' | '_min' | '_max'>
|
||||
> = IsObject<T> extends True ? U : T
|
||||
|
||||
export type GetHavingFields<T> = {
|
||||
[K in keyof T]: Or<
|
||||
Or<Extends<'OR', K>, Extends<'AND', K>>,
|
||||
Extends<'NOT', K>
|
||||
> extends True
|
||||
? // infer is only needed to not hit TS limit
|
||||
// based on the brilliant idea of Pierre-Antoine Mills
|
||||
// https://github.com/microsoft/TypeScript/issues/30188#issuecomment-478938437
|
||||
T[K] extends infer TK
|
||||
? GetHavingFields<UnEnumerate<TK> extends object ? Merge<UnEnumerate<TK>> : never>
|
||||
: never
|
||||
: {} extends FieldPaths<T[K]>
|
||||
? never
|
||||
: K
|
||||
}[keyof T]
|
||||
|
||||
/**
|
||||
* Convert tuple to union
|
||||
*/
|
||||
type _TupleToUnion<T> = T extends (infer E)[] ? E : never
|
||||
type TupleToUnion<K extends readonly any[]> = _TupleToUnion<K>
|
||||
export type MaybeTupleToUnion<T> = T extends any[] ? TupleToUnion<T> : T
|
||||
|
||||
/**
|
||||
* Like `Pick`, but additionally can also accept an array of keys
|
||||
*/
|
||||
export type PickEnumerable<T, K extends Enumerable<keyof T> | keyof T> = Prisma__Pick<T, MaybeTupleToUnion<K>>
|
||||
|
||||
/**
|
||||
* Exclude all keys with underscores
|
||||
*/
|
||||
export type ExcludeUnderscoreKeys<T extends string> = T extends `_${string}` ? never : T
|
||||
|
||||
|
||||
export type FieldRef<Model, FieldType> = runtime.FieldRef<Model, FieldType>
|
||||
|
||||
type FieldRefInputType<Model, FieldType> = Model extends never ? never : FieldRef<Model, FieldType>
|
||||
|
||||
|
||||
export const ModelName = {
|
||||
activity_logs: 'activity_logs',
|
||||
training_menus: 'training_menus',
|
||||
user_recaps: 'user_recaps'
|
||||
} as const
|
||||
|
||||
export type ModelName = (typeof ModelName)[keyof typeof ModelName]
|
||||
|
||||
|
||||
|
||||
export interface TypeMapCb<GlobalOmitOptions = {}> extends runtime.Types.Utils.Fn<{extArgs: runtime.Types.Extensions.InternalArgs }, runtime.Types.Utils.Record<string, any>> {
|
||||
returns: TypeMap<this['params']['extArgs'], GlobalOmitOptions>
|
||||
}
|
||||
|
||||
export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs, GlobalOmitOptions = {}> = {
|
||||
globalOmitOptions: {
|
||||
omit: GlobalOmitOptions
|
||||
}
|
||||
meta: {
|
||||
modelProps: "activity_logs" | "training_menus" | "user_recaps"
|
||||
txIsolationLevel: TransactionIsolationLevel
|
||||
}
|
||||
model: {
|
||||
activity_logs: {
|
||||
payload: Prisma.$activity_logsPayload<ExtArgs>
|
||||
fields: Prisma.activity_logsFieldRefs
|
||||
operations: {
|
||||
findUnique: {
|
||||
args: Prisma.activity_logsFindUniqueArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$activity_logsPayload> | null
|
||||
}
|
||||
findUniqueOrThrow: {
|
||||
args: Prisma.activity_logsFindUniqueOrThrowArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$activity_logsPayload>
|
||||
}
|
||||
findFirst: {
|
||||
args: Prisma.activity_logsFindFirstArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$activity_logsPayload> | null
|
||||
}
|
||||
findFirstOrThrow: {
|
||||
args: Prisma.activity_logsFindFirstOrThrowArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$activity_logsPayload>
|
||||
}
|
||||
findMany: {
|
||||
args: Prisma.activity_logsFindManyArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$activity_logsPayload>[]
|
||||
}
|
||||
create: {
|
||||
args: Prisma.activity_logsCreateArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$activity_logsPayload>
|
||||
}
|
||||
createMany: {
|
||||
args: Prisma.activity_logsCreateManyArgs<ExtArgs>
|
||||
result: BatchPayload
|
||||
}
|
||||
createManyAndReturn: {
|
||||
args: Prisma.activity_logsCreateManyAndReturnArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$activity_logsPayload>[]
|
||||
}
|
||||
delete: {
|
||||
args: Prisma.activity_logsDeleteArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$activity_logsPayload>
|
||||
}
|
||||
update: {
|
||||
args: Prisma.activity_logsUpdateArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$activity_logsPayload>
|
||||
}
|
||||
deleteMany: {
|
||||
args: Prisma.activity_logsDeleteManyArgs<ExtArgs>
|
||||
result: BatchPayload
|
||||
}
|
||||
updateMany: {
|
||||
args: Prisma.activity_logsUpdateManyArgs<ExtArgs>
|
||||
result: BatchPayload
|
||||
}
|
||||
updateManyAndReturn: {
|
||||
args: Prisma.activity_logsUpdateManyAndReturnArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$activity_logsPayload>[]
|
||||
}
|
||||
upsert: {
|
||||
args: Prisma.activity_logsUpsertArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$activity_logsPayload>
|
||||
}
|
||||
aggregate: {
|
||||
args: Prisma.Activity_logsAggregateArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.Optional<Prisma.AggregateActivity_logs>
|
||||
}
|
||||
groupBy: {
|
||||
args: Prisma.activity_logsGroupByArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.Optional<Prisma.Activity_logsGroupByOutputType>[]
|
||||
}
|
||||
count: {
|
||||
args: Prisma.activity_logsCountArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.Optional<Prisma.Activity_logsCountAggregateOutputType> | number
|
||||
}
|
||||
}
|
||||
}
|
||||
training_menus: {
|
||||
payload: Prisma.$training_menusPayload<ExtArgs>
|
||||
fields: Prisma.training_menusFieldRefs
|
||||
operations: {
|
||||
findUnique: {
|
||||
args: Prisma.training_menusFindUniqueArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$training_menusPayload> | null
|
||||
}
|
||||
findUniqueOrThrow: {
|
||||
args: Prisma.training_menusFindUniqueOrThrowArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$training_menusPayload>
|
||||
}
|
||||
findFirst: {
|
||||
args: Prisma.training_menusFindFirstArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$training_menusPayload> | null
|
||||
}
|
||||
findFirstOrThrow: {
|
||||
args: Prisma.training_menusFindFirstOrThrowArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$training_menusPayload>
|
||||
}
|
||||
findMany: {
|
||||
args: Prisma.training_menusFindManyArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$training_menusPayload>[]
|
||||
}
|
||||
create: {
|
||||
args: Prisma.training_menusCreateArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$training_menusPayload>
|
||||
}
|
||||
createMany: {
|
||||
args: Prisma.training_menusCreateManyArgs<ExtArgs>
|
||||
result: BatchPayload
|
||||
}
|
||||
createManyAndReturn: {
|
||||
args: Prisma.training_menusCreateManyAndReturnArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$training_menusPayload>[]
|
||||
}
|
||||
delete: {
|
||||
args: Prisma.training_menusDeleteArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$training_menusPayload>
|
||||
}
|
||||
update: {
|
||||
args: Prisma.training_menusUpdateArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$training_menusPayload>
|
||||
}
|
||||
deleteMany: {
|
||||
args: Prisma.training_menusDeleteManyArgs<ExtArgs>
|
||||
result: BatchPayload
|
||||
}
|
||||
updateMany: {
|
||||
args: Prisma.training_menusUpdateManyArgs<ExtArgs>
|
||||
result: BatchPayload
|
||||
}
|
||||
updateManyAndReturn: {
|
||||
args: Prisma.training_menusUpdateManyAndReturnArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$training_menusPayload>[]
|
||||
}
|
||||
upsert: {
|
||||
args: Prisma.training_menusUpsertArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$training_menusPayload>
|
||||
}
|
||||
aggregate: {
|
||||
args: Prisma.Training_menusAggregateArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.Optional<Prisma.AggregateTraining_menus>
|
||||
}
|
||||
groupBy: {
|
||||
args: Prisma.training_menusGroupByArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.Optional<Prisma.Training_menusGroupByOutputType>[]
|
||||
}
|
||||
count: {
|
||||
args: Prisma.training_menusCountArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.Optional<Prisma.Training_menusCountAggregateOutputType> | number
|
||||
}
|
||||
}
|
||||
}
|
||||
user_recaps: {
|
||||
payload: Prisma.$user_recapsPayload<ExtArgs>
|
||||
fields: Prisma.user_recapsFieldRefs
|
||||
operations: {
|
||||
findUnique: {
|
||||
args: Prisma.user_recapsFindUniqueArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$user_recapsPayload> | null
|
||||
}
|
||||
findUniqueOrThrow: {
|
||||
args: Prisma.user_recapsFindUniqueOrThrowArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$user_recapsPayload>
|
||||
}
|
||||
findFirst: {
|
||||
args: Prisma.user_recapsFindFirstArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$user_recapsPayload> | null
|
||||
}
|
||||
findFirstOrThrow: {
|
||||
args: Prisma.user_recapsFindFirstOrThrowArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$user_recapsPayload>
|
||||
}
|
||||
findMany: {
|
||||
args: Prisma.user_recapsFindManyArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$user_recapsPayload>[]
|
||||
}
|
||||
create: {
|
||||
args: Prisma.user_recapsCreateArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$user_recapsPayload>
|
||||
}
|
||||
createMany: {
|
||||
args: Prisma.user_recapsCreateManyArgs<ExtArgs>
|
||||
result: BatchPayload
|
||||
}
|
||||
createManyAndReturn: {
|
||||
args: Prisma.user_recapsCreateManyAndReturnArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$user_recapsPayload>[]
|
||||
}
|
||||
delete: {
|
||||
args: Prisma.user_recapsDeleteArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$user_recapsPayload>
|
||||
}
|
||||
update: {
|
||||
args: Prisma.user_recapsUpdateArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$user_recapsPayload>
|
||||
}
|
||||
deleteMany: {
|
||||
args: Prisma.user_recapsDeleteManyArgs<ExtArgs>
|
||||
result: BatchPayload
|
||||
}
|
||||
updateMany: {
|
||||
args: Prisma.user_recapsUpdateManyArgs<ExtArgs>
|
||||
result: BatchPayload
|
||||
}
|
||||
updateManyAndReturn: {
|
||||
args: Prisma.user_recapsUpdateManyAndReturnArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$user_recapsPayload>[]
|
||||
}
|
||||
upsert: {
|
||||
args: Prisma.user_recapsUpsertArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$user_recapsPayload>
|
||||
}
|
||||
aggregate: {
|
||||
args: Prisma.User_recapsAggregateArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.Optional<Prisma.AggregateUser_recaps>
|
||||
}
|
||||
groupBy: {
|
||||
args: Prisma.user_recapsGroupByArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.Optional<Prisma.User_recapsGroupByOutputType>[]
|
||||
}
|
||||
count: {
|
||||
args: Prisma.user_recapsCountArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.Optional<Prisma.User_recapsCountAggregateOutputType> | number
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} & {
|
||||
other: {
|
||||
payload: any
|
||||
operations: {
|
||||
$executeRaw: {
|
||||
args: [query: TemplateStringsArray | Sql, ...values: any[]],
|
||||
result: any
|
||||
}
|
||||
$executeRawUnsafe: {
|
||||
args: [query: string, ...values: any[]],
|
||||
result: any
|
||||
}
|
||||
$queryRaw: {
|
||||
args: [query: TemplateStringsArray | Sql, ...values: any[]],
|
||||
result: any
|
||||
}
|
||||
$queryRawUnsafe: {
|
||||
args: [query: string, ...values: any[]],
|
||||
result: any
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enums
|
||||
*/
|
||||
|
||||
export const TransactionIsolationLevel = runtime.makeStrictEnum({
|
||||
ReadUncommitted: 'ReadUncommitted',
|
||||
ReadCommitted: 'ReadCommitted',
|
||||
RepeatableRead: 'RepeatableRead',
|
||||
Serializable: 'Serializable'
|
||||
} as const)
|
||||
|
||||
export type TransactionIsolationLevel = (typeof TransactionIsolationLevel)[keyof typeof TransactionIsolationLevel]
|
||||
|
||||
|
||||
export const Activity_logsScalarFieldEnum = {
|
||||
id: 'id',
|
||||
timestamp: 'timestamp',
|
||||
status: 'status',
|
||||
confidence: 'confidence',
|
||||
details: 'details'
|
||||
} as const
|
||||
|
||||
export type Activity_logsScalarFieldEnum = (typeof Activity_logsScalarFieldEnum)[keyof typeof Activity_logsScalarFieldEnum]
|
||||
|
||||
|
||||
export const Training_menusScalarFieldEnum = {
|
||||
id: 'id',
|
||||
name: 'name',
|
||||
exercises: 'exercises',
|
||||
created_at: 'created_at'
|
||||
} as const
|
||||
|
||||
export type Training_menusScalarFieldEnum = (typeof Training_menusScalarFieldEnum)[keyof typeof Training_menusScalarFieldEnum]
|
||||
|
||||
|
||||
export const User_recapsScalarFieldEnum = {
|
||||
id: 'id',
|
||||
menu_id: 'menu_id',
|
||||
summary: 'summary',
|
||||
completed_at: 'completed_at'
|
||||
} as const
|
||||
|
||||
export type User_recapsScalarFieldEnum = (typeof User_recapsScalarFieldEnum)[keyof typeof User_recapsScalarFieldEnum]
|
||||
|
||||
|
||||
export const SortOrder = {
|
||||
asc: 'asc',
|
||||
desc: 'desc'
|
||||
} as const
|
||||
|
||||
export type SortOrder = (typeof SortOrder)[keyof typeof SortOrder]
|
||||
|
||||
|
||||
export const NullableJsonNullValueInput = {
|
||||
DbNull: DbNull,
|
||||
JsonNull: JsonNull
|
||||
} as const
|
||||
|
||||
export type NullableJsonNullValueInput = (typeof NullableJsonNullValueInput)[keyof typeof NullableJsonNullValueInput]
|
||||
|
||||
|
||||
export const QueryMode = {
|
||||
default: 'default',
|
||||
insensitive: 'insensitive'
|
||||
} as const
|
||||
|
||||
export type QueryMode = (typeof QueryMode)[keyof typeof QueryMode]
|
||||
|
||||
|
||||
export const JsonNullValueFilter = {
|
||||
DbNull: DbNull,
|
||||
JsonNull: JsonNull,
|
||||
AnyNull: AnyNull
|
||||
} as const
|
||||
|
||||
export type JsonNullValueFilter = (typeof JsonNullValueFilter)[keyof typeof JsonNullValueFilter]
|
||||
|
||||
|
||||
export const NullsOrder = {
|
||||
first: 'first',
|
||||
last: 'last'
|
||||
} as const
|
||||
|
||||
export type NullsOrder = (typeof NullsOrder)[keyof typeof NullsOrder]
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Field references
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Reference to a field of type 'Int'
|
||||
*/
|
||||
export type IntFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Int'>
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Reference to a field of type 'Int[]'
|
||||
*/
|
||||
export type ListIntFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Int[]'>
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Reference to a field of type 'DateTime'
|
||||
*/
|
||||
export type DateTimeFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'DateTime'>
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Reference to a field of type 'DateTime[]'
|
||||
*/
|
||||
export type ListDateTimeFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'DateTime[]'>
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Reference to a field of type 'String'
|
||||
*/
|
||||
export type StringFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'String'>
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Reference to a field of type 'String[]'
|
||||
*/
|
||||
export type ListStringFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'String[]'>
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Reference to a field of type 'Json'
|
||||
*/
|
||||
export type JsonFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Json'>
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Reference to a field of type 'QueryMode'
|
||||
*/
|
||||
export type EnumQueryModeFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'QueryMode'>
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Reference to a field of type 'Float'
|
||||
*/
|
||||
export type FloatFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Float'>
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Reference to a field of type 'Float[]'
|
||||
*/
|
||||
export type ListFloatFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Float[]'>
|
||||
|
||||
|
||||
/**
|
||||
* Batch Payload for updateMany & deleteMany & createMany
|
||||
*/
|
||||
export type BatchPayload = {
|
||||
count: number
|
||||
}
|
||||
|
||||
export const defineExtension = runtime.Extensions.defineExtension as unknown as runtime.Types.Extensions.ExtendsHook<"define", TypeMapCb, runtime.Types.Extensions.DefaultArgs>
|
||||
export type DefaultPrismaClient = PrismaClient
|
||||
export type ErrorFormat = 'pretty' | 'colorless' | 'minimal'
|
||||
export type PrismaClientOptions = ({
|
||||
/**
|
||||
* Instance of a Driver Adapter, e.g., like one provided by `@prisma/adapter-pg`.
|
||||
*/
|
||||
adapter: runtime.SqlDriverAdapterFactory
|
||||
accelerateUrl?: never
|
||||
} | {
|
||||
/**
|
||||
* Prisma Accelerate URL allowing the client to connect through Accelerate instead of a direct database.
|
||||
*/
|
||||
accelerateUrl: string
|
||||
adapter?: never
|
||||
}) & {
|
||||
/**
|
||||
* @default "colorless"
|
||||
*/
|
||||
errorFormat?: ErrorFormat
|
||||
/**
|
||||
* @example
|
||||
* ```
|
||||
* // Shorthand for `emit: 'stdout'`
|
||||
* log: ['query', 'info', 'warn', 'error']
|
||||
*
|
||||
* // Emit as events only
|
||||
* log: [
|
||||
* { emit: 'event', level: 'query' },
|
||||
* { emit: 'event', level: 'info' },
|
||||
* { emit: 'event', level: 'warn' }
|
||||
* { emit: 'event', level: 'error' }
|
||||
* ]
|
||||
*
|
||||
* / Emit as events and log to stdout
|
||||
* og: [
|
||||
* { emit: 'stdout', level: 'query' },
|
||||
* { emit: 'stdout', level: 'info' },
|
||||
* { emit: 'stdout', level: 'warn' }
|
||||
* { emit: 'stdout', level: 'error' }
|
||||
*
|
||||
* ```
|
||||
* Read more in our [docs](https://pris.ly/d/logging).
|
||||
*/
|
||||
log?: (LogLevel | LogDefinition)[]
|
||||
/**
|
||||
* The default values for transactionOptions
|
||||
* maxWait ?= 2000
|
||||
* timeout ?= 5000
|
||||
*/
|
||||
transactionOptions?: {
|
||||
maxWait?: number
|
||||
timeout?: number
|
||||
isolationLevel?: TransactionIsolationLevel
|
||||
}
|
||||
/**
|
||||
* Global configuration for omitting model fields by default.
|
||||
*
|
||||
* @example
|
||||
* ```
|
||||
* const prisma = new PrismaClient({
|
||||
* omit: {
|
||||
* user: {
|
||||
* password: true
|
||||
* }
|
||||
* }
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
omit?: GlobalOmitConfig
|
||||
/**
|
||||
* SQL commenter plugins that add metadata to SQL queries as comments.
|
||||
* Comments follow the sqlcommenter format: https://google.github.io/sqlcommenter/
|
||||
*
|
||||
* @example
|
||||
* ```
|
||||
* const prisma = new PrismaClient({
|
||||
* adapter,
|
||||
* comments: [
|
||||
* traceContext(),
|
||||
* queryInsights(),
|
||||
* ],
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
comments?: runtime.SqlCommenterPlugin[]
|
||||
}
|
||||
export type GlobalOmitConfig = {
|
||||
activity_logs?: Prisma.activity_logsOmit
|
||||
training_menus?: Prisma.training_menusOmit
|
||||
user_recaps?: Prisma.user_recapsOmit
|
||||
}
|
||||
|
||||
/* Types for Logging */
|
||||
export type LogLevel = 'info' | 'query' | 'warn' | 'error'
|
||||
export type LogDefinition = {
|
||||
level: LogLevel
|
||||
emit: 'stdout' | 'event'
|
||||
}
|
||||
|
||||
export type CheckIsLogLevel<T> = T extends LogLevel ? T : never;
|
||||
|
||||
export type GetLogType<T> = CheckIsLogLevel<
|
||||
T extends LogDefinition ? T['level'] : T
|
||||
>;
|
||||
|
||||
export type GetEvents<T extends any[]> = T extends Array<LogLevel | LogDefinition>
|
||||
? GetLogType<T[number]>
|
||||
: never;
|
||||
|
||||
export type QueryEvent = {
|
||||
timestamp: Date
|
||||
query: string
|
||||
params: string
|
||||
duration: number
|
||||
target: string
|
||||
}
|
||||
|
||||
export type LogEvent = {
|
||||
timestamp: Date
|
||||
message: string
|
||||
target: string
|
||||
}
|
||||
/* End Types for Logging */
|
||||
|
||||
|
||||
export type PrismaAction =
|
||||
| 'findUnique'
|
||||
| 'findUniqueOrThrow'
|
||||
| 'findMany'
|
||||
| 'findFirst'
|
||||
| 'findFirstOrThrow'
|
||||
| 'create'
|
||||
| 'createMany'
|
||||
| 'createManyAndReturn'
|
||||
| 'update'
|
||||
| 'updateMany'
|
||||
| 'updateManyAndReturn'
|
||||
| 'upsert'
|
||||
| 'delete'
|
||||
| 'deleteMany'
|
||||
| 'executeRaw'
|
||||
| 'queryRaw'
|
||||
| 'aggregate'
|
||||
| 'count'
|
||||
| 'runCommandRaw'
|
||||
| 'findRaw'
|
||||
| 'groupBy'
|
||||
|
||||
/**
|
||||
* `PrismaClient` proxy available in interactive transactions.
|
||||
*/
|
||||
export type TransactionClient = Omit<DefaultPrismaClient, runtime.ITXClientDenyList>
|
||||
|
||||
145
lib/prisma-gen/internal/prismaNamespaceBrowser.ts
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
// @ts-nocheck
|
||||
/*
|
||||
* WARNING: This is an internal file that is subject to change!
|
||||
*
|
||||
* 🛑 Under no circumstances should you import this file directly! 🛑
|
||||
*
|
||||
* All exports from this file are wrapped under a `Prisma` namespace object in the browser.ts file.
|
||||
* While this enables partial backward compatibility, it is not part of the stable public API.
|
||||
*
|
||||
* If you are looking for your Models, Enums, and Input Types, please import them from the respective
|
||||
* model files in the `model` directory!
|
||||
*/
|
||||
|
||||
import * as runtime from "@prisma/client/runtime/index-browser"
|
||||
|
||||
export type * from '../models.js'
|
||||
export type * from './prismaNamespace.js'
|
||||
|
||||
export const Decimal = runtime.Decimal
|
||||
|
||||
|
||||
export const NullTypes = {
|
||||
DbNull: runtime.NullTypes.DbNull as (new (secret: never) => typeof runtime.DbNull),
|
||||
JsonNull: runtime.NullTypes.JsonNull as (new (secret: never) => typeof runtime.JsonNull),
|
||||
AnyNull: runtime.NullTypes.AnyNull as (new (secret: never) => typeof runtime.AnyNull),
|
||||
}
|
||||
/**
|
||||
* Helper for filtering JSON entries that have `null` on the database (empty on the db)
|
||||
*
|
||||
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
|
||||
*/
|
||||
export const DbNull = runtime.DbNull
|
||||
|
||||
/**
|
||||
* Helper for filtering JSON entries that have JSON `null` values (not empty on the db)
|
||||
*
|
||||
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
|
||||
*/
|
||||
export const JsonNull = runtime.JsonNull
|
||||
|
||||
/**
|
||||
* Helper for filtering JSON entries that are `Prisma.DbNull` or `Prisma.JsonNull`
|
||||
*
|
||||
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
|
||||
*/
|
||||
export const AnyNull = runtime.AnyNull
|
||||
|
||||
|
||||
export const ModelName = {
|
||||
activity_logs: 'activity_logs',
|
||||
training_menus: 'training_menus',
|
||||
user_recaps: 'user_recaps'
|
||||
} as const
|
||||
|
||||
export type ModelName = (typeof ModelName)[keyof typeof ModelName]
|
||||
|
||||
/*
|
||||
* Enums
|
||||
*/
|
||||
|
||||
export const TransactionIsolationLevel = {
|
||||
ReadUncommitted: 'ReadUncommitted',
|
||||
ReadCommitted: 'ReadCommitted',
|
||||
RepeatableRead: 'RepeatableRead',
|
||||
Serializable: 'Serializable'
|
||||
} as const
|
||||
|
||||
export type TransactionIsolationLevel = (typeof TransactionIsolationLevel)[keyof typeof TransactionIsolationLevel]
|
||||
|
||||
|
||||
export const Activity_logsScalarFieldEnum = {
|
||||
id: 'id',
|
||||
timestamp: 'timestamp',
|
||||
status: 'status',
|
||||
confidence: 'confidence',
|
||||
details: 'details'
|
||||
} as const
|
||||
|
||||
export type Activity_logsScalarFieldEnum = (typeof Activity_logsScalarFieldEnum)[keyof typeof Activity_logsScalarFieldEnum]
|
||||
|
||||
|
||||
export const Training_menusScalarFieldEnum = {
|
||||
id: 'id',
|
||||
name: 'name',
|
||||
exercises: 'exercises',
|
||||
created_at: 'created_at'
|
||||
} as const
|
||||
|
||||
export type Training_menusScalarFieldEnum = (typeof Training_menusScalarFieldEnum)[keyof typeof Training_menusScalarFieldEnum]
|
||||
|
||||
|
||||
export const User_recapsScalarFieldEnum = {
|
||||
id: 'id',
|
||||
menu_id: 'menu_id',
|
||||
summary: 'summary',
|
||||
completed_at: 'completed_at'
|
||||
} as const
|
||||
|
||||
export type User_recapsScalarFieldEnum = (typeof User_recapsScalarFieldEnum)[keyof typeof User_recapsScalarFieldEnum]
|
||||
|
||||
|
||||
export const SortOrder = {
|
||||
asc: 'asc',
|
||||
desc: 'desc'
|
||||
} as const
|
||||
|
||||
export type SortOrder = (typeof SortOrder)[keyof typeof SortOrder]
|
||||
|
||||
|
||||
export const NullableJsonNullValueInput = {
|
||||
DbNull: 'DbNull',
|
||||
JsonNull: 'JsonNull'
|
||||
} as const
|
||||
|
||||
export type NullableJsonNullValueInput = (typeof NullableJsonNullValueInput)[keyof typeof NullableJsonNullValueInput]
|
||||
|
||||
|
||||
export const QueryMode = {
|
||||
default: 'default',
|
||||
insensitive: 'insensitive'
|
||||
} as const
|
||||
|
||||
export type QueryMode = (typeof QueryMode)[keyof typeof QueryMode]
|
||||
|
||||
|
||||
export const JsonNullValueFilter = {
|
||||
DbNull: 'DbNull',
|
||||
JsonNull: 'JsonNull',
|
||||
AnyNull: 'AnyNull'
|
||||
} as const
|
||||
|
||||
export type JsonNullValueFilter = (typeof JsonNullValueFilter)[keyof typeof JsonNullValueFilter]
|
||||
|
||||
|
||||
export const NullsOrder = {
|
||||
first: 'first',
|
||||
last: 'last'
|
||||
} as const
|
||||
|
||||
export type NullsOrder = (typeof NullsOrder)[keyof typeof NullsOrder]
|
||||
|
||||
14
lib/prisma-gen/models.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
// @ts-nocheck
|
||||
/*
|
||||
* This is a barrel export file for all models and their related types.
|
||||
*
|
||||
* 🟢 You can import this file directly.
|
||||
*/
|
||||
export type * from './models/activity_logs.js'
|
||||
export type * from './models/training_menus.js'
|
||||
export type * from './models/user_recaps.js'
|
||||
export type * from './commonInputTypes.js'
|
||||
1195
lib/prisma-gen/models/activity_logs.ts
Normal file
1325
lib/prisma-gen/models/training_menus.ts
Normal file
1371
lib/prisma-gen/models/user_recaps.ts
Normal file
10
lib/prisma.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
// @ts-nocheck
|
||||
import { PrismaClient } from '../app/generated/client/client';
|
||||
|
||||
const globalForPrisma = global as unknown as { prisma: PrismaClient };
|
||||
|
||||
export const prisma =
|
||||
globalForPrisma.prisma ||
|
||||
new PrismaClient();
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;
|
||||
34
lib/prisma/client/browser.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
// @ts-nocheck
|
||||
/*
|
||||
* This file should be your main import to use Prisma-related types and utilities in a browser.
|
||||
* Use it to get access to models, enums, and input types.
|
||||
*
|
||||
* This file does not contain a `PrismaClient` class, nor several other helpers that are intended as server-side only.
|
||||
* See `client.ts` for the standard, server-side entry point.
|
||||
*
|
||||
* 🟢 You can import this file directly.
|
||||
*/
|
||||
|
||||
import * as Prisma from './internal/prismaNamespaceBrowser'
|
||||
export { Prisma }
|
||||
export * as $Enums from './enums'
|
||||
export * from './enums';
|
||||
/**
|
||||
* Model activity_logs
|
||||
*
|
||||
*/
|
||||
export type activity_logs = Prisma.activity_logsModel
|
||||
/**
|
||||
* Model training_menus
|
||||
*
|
||||
*/
|
||||
export type training_menus = Prisma.training_menusModel
|
||||
/**
|
||||
* Model user_recaps
|
||||
*
|
||||
*/
|
||||
export type user_recaps = Prisma.user_recapsModel
|
||||
56
lib/prisma/client/client.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
// @ts-nocheck
|
||||
/*
|
||||
* This file should be your main import to use Prisma. Through it you get access to all the models, enums, and input types.
|
||||
* If you're looking for something you can import in the client-side of your application, please refer to the `browser.ts` file instead.
|
||||
*
|
||||
* 🟢 You can import this file directly.
|
||||
*/
|
||||
|
||||
import * as process from 'node:process'
|
||||
import * as path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
globalThis['__dirname'] = path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
import * as runtime from "@prisma/client/runtime/client"
|
||||
import * as $Enums from "./enums"
|
||||
import * as $Class from "./internal/class"
|
||||
import * as Prisma from "./internal/prismaNamespace"
|
||||
|
||||
export * as $Enums from './enums'
|
||||
export * from "./enums"
|
||||
/**
|
||||
* ## Prisma Client
|
||||
*
|
||||
* Type-safe database client for TypeScript
|
||||
* @example
|
||||
* ```
|
||||
* const prisma = new PrismaClient()
|
||||
* // Fetch zero or more Activity_logs
|
||||
* const activity_logs = await prisma.activity_logs.findMany()
|
||||
* ```
|
||||
*
|
||||
* Read more in our [docs](https://pris.ly/d/client).
|
||||
*/
|
||||
export const PrismaClient = $Class.getPrismaClientClass()
|
||||
export type PrismaClient<LogOpts extends Prisma.LogLevel = never, OmitOpts extends Prisma.PrismaClientOptions["omit"] = Prisma.PrismaClientOptions["omit"], ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = $Class.PrismaClient<LogOpts, OmitOpts, ExtArgs>
|
||||
export { Prisma }
|
||||
|
||||
/**
|
||||
* Model activity_logs
|
||||
*
|
||||
*/
|
||||
export type activity_logs = Prisma.activity_logsModel
|
||||
/**
|
||||
* Model training_menus
|
||||
*
|
||||
*/
|
||||
export type training_menus = Prisma.training_menusModel
|
||||
/**
|
||||
* Model user_recaps
|
||||
*
|
||||
*/
|
||||
export type user_recaps = Prisma.user_recapsModel
|
||||
341
lib/prisma/client/commonInputTypes.ts
Normal file
|
|
@ -0,0 +1,341 @@
|
|||
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
// @ts-nocheck
|
||||
/*
|
||||
* This file exports various common sort, input & filter types that are not directly linked to a particular model.
|
||||
*
|
||||
* 🟢 You can import this file directly.
|
||||
*/
|
||||
|
||||
import type * as runtime from "@prisma/client/runtime/client"
|
||||
import * as $Enums from "./enums"
|
||||
import type * as Prisma from "./internal/prismaNamespace"
|
||||
|
||||
|
||||
export type IntFilter<$PrismaModel = never> = {
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
|
||||
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
|
||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedIntFilter<$PrismaModel> | number
|
||||
}
|
||||
|
||||
export type DateTimeNullableFilter<$PrismaModel = never> = {
|
||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
|
||||
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
|
||||
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
|
||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> | Date | string | null
|
||||
}
|
||||
|
||||
export type StringNullableFilter<$PrismaModel = never> = {
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
||||
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
|
||||
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
mode?: Prisma.QueryMode
|
||||
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null
|
||||
}
|
||||
|
||||
export type JsonNullableFilter<$PrismaModel = never> =
|
||||
| Prisma.PatchUndefined<
|
||||
Prisma.Either<Required<JsonNullableFilterBase<$PrismaModel>>, Exclude<keyof Required<JsonNullableFilterBase<$PrismaModel>>, 'path'>>,
|
||||
Required<JsonNullableFilterBase<$PrismaModel>>
|
||||
>
|
||||
| Prisma.OptionalFlat<Omit<Required<JsonNullableFilterBase<$PrismaModel>>, 'path'>>
|
||||
|
||||
export type JsonNullableFilterBase<$PrismaModel = never> = {
|
||||
equals?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
|
||||
path?: string[]
|
||||
mode?: Prisma.QueryMode | Prisma.EnumQueryModeFieldRefInput<$PrismaModel>
|
||||
string_contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
string_starts_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
string_ends_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
array_starts_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
|
||||
array_ends_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
|
||||
array_contains?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
|
||||
lt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
|
||||
lte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
|
||||
gt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
|
||||
gte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
|
||||
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
|
||||
}
|
||||
|
||||
export type SortOrderInput = {
|
||||
sort: Prisma.SortOrder
|
||||
nulls?: Prisma.NullsOrder
|
||||
}
|
||||
|
||||
export type IntWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
|
||||
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
|
||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedIntWithAggregatesFilter<$PrismaModel> | number
|
||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_avg?: Prisma.NestedFloatFilter<$PrismaModel>
|
||||
_sum?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type DateTimeNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
|
||||
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
|
||||
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
|
||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedDateTimeNullableWithAggregatesFilter<$PrismaModel> | Date | string | null
|
||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type StringNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
||||
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
|
||||
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
mode?: Prisma.QueryMode
|
||||
not?: Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel> | string | null
|
||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type JsonNullableWithAggregatesFilter<$PrismaModel = never> =
|
||||
| Prisma.PatchUndefined<
|
||||
Prisma.Either<Required<JsonNullableWithAggregatesFilterBase<$PrismaModel>>, Exclude<keyof Required<JsonNullableWithAggregatesFilterBase<$PrismaModel>>, 'path'>>,
|
||||
Required<JsonNullableWithAggregatesFilterBase<$PrismaModel>>
|
||||
>
|
||||
| Prisma.OptionalFlat<Omit<Required<JsonNullableWithAggregatesFilterBase<$PrismaModel>>, 'path'>>
|
||||
|
||||
export type JsonNullableWithAggregatesFilterBase<$PrismaModel = never> = {
|
||||
equals?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
|
||||
path?: string[]
|
||||
mode?: Prisma.QueryMode | Prisma.EnumQueryModeFieldRefInput<$PrismaModel>
|
||||
string_contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
string_starts_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
string_ends_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
array_starts_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
|
||||
array_ends_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
|
||||
array_contains?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
|
||||
lt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
|
||||
lte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
|
||||
gt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
|
||||
gte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
|
||||
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
|
||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedJsonNullableFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedJsonNullableFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type IntNullableFilter<$PrismaModel = never> = {
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
|
||||
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
|
||||
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
|
||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedIntNullableFilter<$PrismaModel> | number | null
|
||||
}
|
||||
|
||||
export type IntNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
|
||||
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
|
||||
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
|
||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedIntNullableWithAggregatesFilter<$PrismaModel> | number | null
|
||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
_avg?: Prisma.NestedFloatNullableFilter<$PrismaModel>
|
||||
_sum?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type NestedIntFilter<$PrismaModel = never> = {
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
|
||||
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
|
||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedIntFilter<$PrismaModel> | number
|
||||
}
|
||||
|
||||
export type NestedDateTimeNullableFilter<$PrismaModel = never> = {
|
||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
|
||||
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
|
||||
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
|
||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> | Date | string | null
|
||||
}
|
||||
|
||||
export type NestedStringNullableFilter<$PrismaModel = never> = {
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
||||
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
|
||||
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null
|
||||
}
|
||||
|
||||
export type NestedIntWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
|
||||
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
|
||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedIntWithAggregatesFilter<$PrismaModel> | number
|
||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_avg?: Prisma.NestedFloatFilter<$PrismaModel>
|
||||
_sum?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type NestedFloatFilter<$PrismaModel = never> = {
|
||||
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||
in?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel>
|
||||
notIn?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel>
|
||||
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedFloatFilter<$PrismaModel> | number
|
||||
}
|
||||
|
||||
export type NestedDateTimeNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
|
||||
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
|
||||
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
|
||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedDateTimeNullableWithAggregatesFilter<$PrismaModel> | Date | string | null
|
||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type NestedIntNullableFilter<$PrismaModel = never> = {
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
|
||||
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
|
||||
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
|
||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedIntNullableFilter<$PrismaModel> | number | null
|
||||
}
|
||||
|
||||
export type NestedStringNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
||||
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
|
||||
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel> | string | null
|
||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type NestedJsonNullableFilter<$PrismaModel = never> =
|
||||
| Prisma.PatchUndefined<
|
||||
Prisma.Either<Required<NestedJsonNullableFilterBase<$PrismaModel>>, Exclude<keyof Required<NestedJsonNullableFilterBase<$PrismaModel>>, 'path'>>,
|
||||
Required<NestedJsonNullableFilterBase<$PrismaModel>>
|
||||
>
|
||||
| Prisma.OptionalFlat<Omit<Required<NestedJsonNullableFilterBase<$PrismaModel>>, 'path'>>
|
||||
|
||||
export type NestedJsonNullableFilterBase<$PrismaModel = never> = {
|
||||
equals?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
|
||||
path?: string[]
|
||||
mode?: Prisma.QueryMode | Prisma.EnumQueryModeFieldRefInput<$PrismaModel>
|
||||
string_contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
string_starts_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
string_ends_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
array_starts_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
|
||||
array_ends_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
|
||||
array_contains?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
|
||||
lt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
|
||||
lte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
|
||||
gt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
|
||||
gte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
|
||||
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
|
||||
}
|
||||
|
||||
export type NestedIntNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
|
||||
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
|
||||
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
|
||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedIntNullableWithAggregatesFilter<$PrismaModel> | number | null
|
||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
_avg?: Prisma.NestedFloatNullableFilter<$PrismaModel>
|
||||
_sum?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type NestedFloatNullableFilter<$PrismaModel = never> = {
|
||||
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel> | null
|
||||
in?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> | null
|
||||
notIn?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> | null
|
||||
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedFloatNullableFilter<$PrismaModel> | number | null
|
||||
}
|
||||
|
||||
|
||||
15
lib/prisma/client/enums.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
// @ts-nocheck
|
||||
/*
|
||||
* This file exports all enum related types from the schema.
|
||||
*
|
||||
* 🟢 You can import this file directly.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
// This file is empty because there are no enums in the schema.
|
||||
export {}
|
||||
210
lib/prisma/client/internal/class.ts
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
// @ts-nocheck
|
||||
/*
|
||||
* WARNING: This is an internal file that is subject to change!
|
||||
*
|
||||
* 🛑 Under no circumstances should you import this file directly! 🛑
|
||||
*
|
||||
* Please import the `PrismaClient` class from the `client.ts` file instead.
|
||||
*/
|
||||
|
||||
import * as runtime from "@prisma/client/runtime/client"
|
||||
import type * as Prisma from "./prismaNamespace"
|
||||
|
||||
|
||||
const config: runtime.GetPrismaClientConfig = {
|
||||
"previewFeatures": [],
|
||||
"clientVersion": "7.2.0",
|
||||
"engineVersion": "0c8ef2ce45c83248ab3df073180d5eda9e8be7a3",
|
||||
"activeProvider": "postgresql",
|
||||
"inlineSchema": "generator client {\n provider = \"prisma-client\"\n output = \"../lib/prisma/client\"\n}\n\ndatasource db {\n provider = \"postgresql\"\n}\n\nmodel activity_logs {\n id Int @id @default(autoincrement())\n timestamp DateTime? @db.Timestamp(6)\n status String? @db.VarChar\n confidence String? @db.VarChar\n details Json?\n\n @@index([id], map: \"ix_activity_logs_id\")\n}\n\nmodel training_menus {\n id Int @id @default(autoincrement())\n name String? @db.VarChar\n exercises Json?\n created_at DateTime? @db.Timestamp(6)\n user_recaps user_recaps[]\n\n @@index([id], map: \"ix_training_menus_id\")\n @@index([name], map: \"ix_training_menus_name\")\n}\n\nmodel user_recaps {\n id Int @id @default(autoincrement())\n menu_id Int?\n summary Json?\n completed_at DateTime? @db.Timestamp(6)\n training_menus training_menus? @relation(fields: [menu_id], references: [id], onDelete: NoAction, onUpdate: NoAction)\n\n @@index([id], map: \"ix_user_recaps_id\")\n}\n",
|
||||
"runtimeDataModel": {
|
||||
"models": {},
|
||||
"enums": {},
|
||||
"types": {}
|
||||
}
|
||||
}
|
||||
|
||||
config.runtimeDataModel = JSON.parse("{\"models\":{\"activity_logs\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"timestamp\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"status\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"confidence\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"details\",\"kind\":\"scalar\",\"type\":\"Json\"}],\"dbName\":null},\"training_menus\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"exercises\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"created_at\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"user_recaps\",\"kind\":\"object\",\"type\":\"user_recaps\",\"relationName\":\"training_menusTouser_recaps\"}],\"dbName\":null},\"user_recaps\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"menu_id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"summary\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"completed_at\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"training_menus\",\"kind\":\"object\",\"type\":\"training_menus\",\"relationName\":\"training_menusTouser_recaps\"}],\"dbName\":null}},\"enums\":{},\"types\":{}}")
|
||||
|
||||
async function decodeBase64AsWasm(wasmBase64: string): Promise<WebAssembly.Module> {
|
||||
const { Buffer } = await import('node:buffer')
|
||||
const wasmArray = Buffer.from(wasmBase64, 'base64')
|
||||
return new WebAssembly.Module(wasmArray)
|
||||
}
|
||||
|
||||
config.compilerWasm = {
|
||||
getRuntime: async () => await import("@prisma/client/runtime/query_compiler_bg.postgresql.mjs"),
|
||||
|
||||
getQueryCompilerWasmModule: async () => {
|
||||
const { wasm } = await import("@prisma/client/runtime/query_compiler_bg.postgresql.wasm-base64.mjs")
|
||||
return await decodeBase64AsWasm(wasm)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
export type LogOptions<ClientOptions extends Prisma.PrismaClientOptions> =
|
||||
'log' extends keyof ClientOptions ? ClientOptions['log'] extends Array<Prisma.LogLevel | Prisma.LogDefinition> ? Prisma.GetEvents<ClientOptions['log']> : never : never
|
||||
|
||||
export interface PrismaClientConstructor {
|
||||
/**
|
||||
* ## Prisma Client
|
||||
*
|
||||
* Type-safe database client for TypeScript
|
||||
* @example
|
||||
* ```
|
||||
* const prisma = new PrismaClient()
|
||||
* // Fetch zero or more Activity_logs
|
||||
* const activity_logs = await prisma.activity_logs.findMany()
|
||||
* ```
|
||||
*
|
||||
* Read more in our [docs](https://pris.ly/d/client).
|
||||
*/
|
||||
|
||||
new <
|
||||
Options extends Prisma.PrismaClientOptions = Prisma.PrismaClientOptions,
|
||||
LogOpts extends LogOptions<Options> = LogOptions<Options>,
|
||||
OmitOpts extends Prisma.PrismaClientOptions['omit'] = Options extends { omit: infer U } ? U : Prisma.PrismaClientOptions['omit'],
|
||||
ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs
|
||||
>(options: Prisma.Subset<Options, Prisma.PrismaClientOptions> ): PrismaClient<LogOpts, OmitOpts, ExtArgs>
|
||||
}
|
||||
|
||||
/**
|
||||
* ## Prisma Client
|
||||
*
|
||||
* Type-safe database client for TypeScript
|
||||
* @example
|
||||
* ```
|
||||
* const prisma = new PrismaClient()
|
||||
* // Fetch zero or more Activity_logs
|
||||
* const activity_logs = await prisma.activity_logs.findMany()
|
||||
* ```
|
||||
*
|
||||
* Read more in our [docs](https://pris.ly/d/client).
|
||||
*/
|
||||
|
||||
export interface PrismaClient<
|
||||
in LogOpts extends Prisma.LogLevel = never,
|
||||
in out OmitOpts extends Prisma.PrismaClientOptions['omit'] = undefined,
|
||||
in out ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs
|
||||
> {
|
||||
[K: symbol]: { types: Prisma.TypeMap<ExtArgs>['other'] }
|
||||
|
||||
$on<V extends LogOpts>(eventType: V, callback: (event: V extends 'query' ? Prisma.QueryEvent : Prisma.LogEvent) => void): PrismaClient;
|
||||
|
||||
/**
|
||||
* Connect with the database
|
||||
*/
|
||||
$connect(): runtime.Types.Utils.JsPromise<void>;
|
||||
|
||||
/**
|
||||
* Disconnect from the database
|
||||
*/
|
||||
$disconnect(): runtime.Types.Utils.JsPromise<void>;
|
||||
|
||||
/**
|
||||
* Executes a prepared raw query and returns the number of affected rows.
|
||||
* @example
|
||||
* ```
|
||||
* const result = await prisma.$executeRaw`UPDATE User SET cool = ${true} WHERE email = ${'user@email.com'};`
|
||||
* ```
|
||||
*
|
||||
* Read more in our [docs](https://pris.ly/d/raw-queries).
|
||||
*/
|
||||
$executeRaw<T = unknown>(query: TemplateStringsArray | Prisma.Sql, ...values: any[]): Prisma.PrismaPromise<number>;
|
||||
|
||||
/**
|
||||
* Executes a raw query and returns the number of affected rows.
|
||||
* Susceptible to SQL injections, see documentation.
|
||||
* @example
|
||||
* ```
|
||||
* const result = await prisma.$executeRawUnsafe('UPDATE User SET cool = $1 WHERE email = $2 ;', true, 'user@email.com')
|
||||
* ```
|
||||
*
|
||||
* Read more in our [docs](https://pris.ly/d/raw-queries).
|
||||
*/
|
||||
$executeRawUnsafe<T = unknown>(query: string, ...values: any[]): Prisma.PrismaPromise<number>;
|
||||
|
||||
/**
|
||||
* Performs a prepared raw query and returns the `SELECT` data.
|
||||
* @example
|
||||
* ```
|
||||
* const result = await prisma.$queryRaw`SELECT * FROM User WHERE id = ${1} OR email = ${'user@email.com'};`
|
||||
* ```
|
||||
*
|
||||
* Read more in our [docs](https://pris.ly/d/raw-queries).
|
||||
*/
|
||||
$queryRaw<T = unknown>(query: TemplateStringsArray | Prisma.Sql, ...values: any[]): Prisma.PrismaPromise<T>;
|
||||
|
||||
/**
|
||||
* Performs a raw query and returns the `SELECT` data.
|
||||
* Susceptible to SQL injections, see documentation.
|
||||
* @example
|
||||
* ```
|
||||
* const result = await prisma.$queryRawUnsafe('SELECT * FROM User WHERE id = $1 OR email = $2;', 1, 'user@email.com')
|
||||
* ```
|
||||
*
|
||||
* Read more in our [docs](https://pris.ly/d/raw-queries).
|
||||
*/
|
||||
$queryRawUnsafe<T = unknown>(query: string, ...values: any[]): Prisma.PrismaPromise<T>;
|
||||
|
||||
|
||||
/**
|
||||
* Allows the running of a sequence of read/write operations that are guaranteed to either succeed or fail as a whole.
|
||||
* @example
|
||||
* ```
|
||||
* const [george, bob, alice] = await prisma.$transaction([
|
||||
* prisma.user.create({ data: { name: 'George' } }),
|
||||
* prisma.user.create({ data: { name: 'Bob' } }),
|
||||
* prisma.user.create({ data: { name: 'Alice' } }),
|
||||
* ])
|
||||
* ```
|
||||
*
|
||||
* Read more in our [docs](https://www.prisma.io/docs/concepts/components/prisma-client/transactions).
|
||||
*/
|
||||
$transaction<P extends Prisma.PrismaPromise<any>[]>(arg: [...P], options?: { isolationLevel?: Prisma.TransactionIsolationLevel }): runtime.Types.Utils.JsPromise<runtime.Types.Utils.UnwrapTuple<P>>
|
||||
|
||||
$transaction<R>(fn: (prisma: Omit<PrismaClient, runtime.ITXClientDenyList>) => runtime.Types.Utils.JsPromise<R>, options?: { maxWait?: number, timeout?: number, isolationLevel?: Prisma.TransactionIsolationLevel }): runtime.Types.Utils.JsPromise<R>
|
||||
|
||||
$extends: runtime.Types.Extensions.ExtendsHook<"extends", Prisma.TypeMapCb<OmitOpts>, ExtArgs, runtime.Types.Utils.Call<Prisma.TypeMapCb<OmitOpts>, {
|
||||
extArgs: ExtArgs
|
||||
}>>
|
||||
|
||||
/**
|
||||
* `prisma.activity_logs`: Exposes CRUD operations for the **activity_logs** model.
|
||||
* Example usage:
|
||||
* ```ts
|
||||
* // Fetch zero or more Activity_logs
|
||||
* const activity_logs = await prisma.activity_logs.findMany()
|
||||
* ```
|
||||
*/
|
||||
get activity_logs(): Prisma.activity_logsDelegate<ExtArgs, { omit: OmitOpts }>;
|
||||
|
||||
/**
|
||||
* `prisma.training_menus`: Exposes CRUD operations for the **training_menus** model.
|
||||
* Example usage:
|
||||
* ```ts
|
||||
* // Fetch zero or more Training_menus
|
||||
* const training_menus = await prisma.training_menus.findMany()
|
||||
* ```
|
||||
*/
|
||||
get training_menus(): Prisma.training_menusDelegate<ExtArgs, { omit: OmitOpts }>;
|
||||
|
||||
/**
|
||||
* `prisma.user_recaps`: Exposes CRUD operations for the **user_recaps** model.
|
||||
* Example usage:
|
||||
* ```ts
|
||||
* // Fetch zero or more User_recaps
|
||||
* const user_recaps = await prisma.user_recaps.findMany()
|
||||
* ```
|
||||
*/
|
||||
get user_recaps(): Prisma.user_recapsDelegate<ExtArgs, { omit: OmitOpts }>;
|
||||
}
|
||||
|
||||
export function getPrismaClientClass(): PrismaClientConstructor {
|
||||
return runtime.getPrismaClient(config) as unknown as PrismaClientConstructor
|
||||
}
|
||||
977
lib/prisma/client/internal/prismaNamespace.ts
Normal file
|
|
@ -0,0 +1,977 @@
|
|||
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
// @ts-nocheck
|
||||
/*
|
||||
* WARNING: This is an internal file that is subject to change!
|
||||
*
|
||||
* 🛑 Under no circumstances should you import this file directly! 🛑
|
||||
*
|
||||
* All exports from this file are wrapped under a `Prisma` namespace object in the client.ts file.
|
||||
* While this enables partial backward compatibility, it is not part of the stable public API.
|
||||
*
|
||||
* If you are looking for your Models, Enums, and Input Types, please import them from the respective
|
||||
* model files in the `model` directory!
|
||||
*/
|
||||
|
||||
import * as runtime from "@prisma/client/runtime/client"
|
||||
import type * as Prisma from "../models"
|
||||
import { type PrismaClient } from "./class"
|
||||
|
||||
export type * from '../models'
|
||||
|
||||
export type DMMF = typeof runtime.DMMF
|
||||
|
||||
export type PrismaPromise<T> = runtime.Types.Public.PrismaPromise<T>
|
||||
|
||||
/**
|
||||
* Prisma Errors
|
||||
*/
|
||||
|
||||
export const PrismaClientKnownRequestError = runtime.PrismaClientKnownRequestError
|
||||
export type PrismaClientKnownRequestError = runtime.PrismaClientKnownRequestError
|
||||
|
||||
export const PrismaClientUnknownRequestError = runtime.PrismaClientUnknownRequestError
|
||||
export type PrismaClientUnknownRequestError = runtime.PrismaClientUnknownRequestError
|
||||
|
||||
export const PrismaClientRustPanicError = runtime.PrismaClientRustPanicError
|
||||
export type PrismaClientRustPanicError = runtime.PrismaClientRustPanicError
|
||||
|
||||
export const PrismaClientInitializationError = runtime.PrismaClientInitializationError
|
||||
export type PrismaClientInitializationError = runtime.PrismaClientInitializationError
|
||||
|
||||
export const PrismaClientValidationError = runtime.PrismaClientValidationError
|
||||
export type PrismaClientValidationError = runtime.PrismaClientValidationError
|
||||
|
||||
/**
|
||||
* Re-export of sql-template-tag
|
||||
*/
|
||||
export const sql = runtime.sqltag
|
||||
export const empty = runtime.empty
|
||||
export const join = runtime.join
|
||||
export const raw = runtime.raw
|
||||
export const Sql = runtime.Sql
|
||||
export type Sql = runtime.Sql
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Decimal.js
|
||||
*/
|
||||
export const Decimal = runtime.Decimal
|
||||
export type Decimal = runtime.Decimal
|
||||
|
||||
export type DecimalJsLike = runtime.DecimalJsLike
|
||||
|
||||
/**
|
||||
* Extensions
|
||||
*/
|
||||
export type Extension = runtime.Types.Extensions.UserArgs
|
||||
export const getExtensionContext = runtime.Extensions.getExtensionContext
|
||||
export type Args<T, F extends runtime.Operation> = runtime.Types.Public.Args<T, F>
|
||||
export type Payload<T, F extends runtime.Operation = never> = runtime.Types.Public.Payload<T, F>
|
||||
export type Result<T, A, F extends runtime.Operation> = runtime.Types.Public.Result<T, A, F>
|
||||
export type Exact<A, W> = runtime.Types.Public.Exact<A, W>
|
||||
|
||||
export type PrismaVersion = {
|
||||
client: string
|
||||
engine: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Prisma Client JS version: 7.2.0
|
||||
* Query Engine version: 0c8ef2ce45c83248ab3df073180d5eda9e8be7a3
|
||||
*/
|
||||
export const prismaVersion: PrismaVersion = {
|
||||
client: "7.2.0",
|
||||
engine: "0c8ef2ce45c83248ab3df073180d5eda9e8be7a3"
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility Types
|
||||
*/
|
||||
|
||||
export type Bytes = runtime.Bytes
|
||||
export type JsonObject = runtime.JsonObject
|
||||
export type JsonArray = runtime.JsonArray
|
||||
export type JsonValue = runtime.JsonValue
|
||||
export type InputJsonObject = runtime.InputJsonObject
|
||||
export type InputJsonArray = runtime.InputJsonArray
|
||||
export type InputJsonValue = runtime.InputJsonValue
|
||||
|
||||
|
||||
export const NullTypes = {
|
||||
DbNull: runtime.NullTypes.DbNull as (new (secret: never) => typeof runtime.DbNull),
|
||||
JsonNull: runtime.NullTypes.JsonNull as (new (secret: never) => typeof runtime.JsonNull),
|
||||
AnyNull: runtime.NullTypes.AnyNull as (new (secret: never) => typeof runtime.AnyNull),
|
||||
}
|
||||
/**
|
||||
* Helper for filtering JSON entries that have `null` on the database (empty on the db)
|
||||
*
|
||||
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
|
||||
*/
|
||||
export const DbNull = runtime.DbNull
|
||||
|
||||
/**
|
||||
* Helper for filtering JSON entries that have JSON `null` values (not empty on the db)
|
||||
*
|
||||
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
|
||||
*/
|
||||
export const JsonNull = runtime.JsonNull
|
||||
|
||||
/**
|
||||
* Helper for filtering JSON entries that are `Prisma.DbNull` or `Prisma.JsonNull`
|
||||
*
|
||||
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
|
||||
*/
|
||||
export const AnyNull = runtime.AnyNull
|
||||
|
||||
|
||||
type SelectAndInclude = {
|
||||
select: any
|
||||
include: any
|
||||
}
|
||||
|
||||
type SelectAndOmit = {
|
||||
select: any
|
||||
omit: any
|
||||
}
|
||||
|
||||
/**
|
||||
* From T, pick a set of properties whose keys are in the union K
|
||||
*/
|
||||
type Prisma__Pick<T, K extends keyof T> = {
|
||||
[P in K]: T[P];
|
||||
};
|
||||
|
||||
export type Enumerable<T> = T | Array<T>;
|
||||
|
||||
/**
|
||||
* Subset
|
||||
* @desc From `T` pick properties that exist in `U`. Simple version of Intersection
|
||||
*/
|
||||
export type Subset<T, U> = {
|
||||
[key in keyof T]: key extends keyof U ? T[key] : never;
|
||||
};
|
||||
|
||||
/**
|
||||
* SelectSubset
|
||||
* @desc From `T` pick properties that exist in `U`. Simple version of Intersection.
|
||||
* Additionally, it validates, if both select and include are present. If the case, it errors.
|
||||
*/
|
||||
export type SelectSubset<T, U> = {
|
||||
[key in keyof T]: key extends keyof U ? T[key] : never
|
||||
} &
|
||||
(T extends SelectAndInclude
|
||||
? 'Please either choose `select` or `include`.'
|
||||
: T extends SelectAndOmit
|
||||
? 'Please either choose `select` or `omit`.'
|
||||
: {})
|
||||
|
||||
/**
|
||||
* Subset + Intersection
|
||||
* @desc From `T` pick properties that exist in `U` and intersect `K`
|
||||
*/
|
||||
export type SubsetIntersection<T, U, K> = {
|
||||
[key in keyof T]: key extends keyof U ? T[key] : never
|
||||
} &
|
||||
K
|
||||
|
||||
type Without<T, U> = { [P in Exclude<keyof T, keyof U>]?: never };
|
||||
|
||||
/**
|
||||
* XOR is needed to have a real mutually exclusive union type
|
||||
* https://stackoverflow.com/questions/42123407/does-typescript-support-mutually-exclusive-types
|
||||
*/
|
||||
export type XOR<T, U> =
|
||||
T extends object ?
|
||||
U extends object ?
|
||||
(Without<T, U> & U) | (Without<U, T> & T)
|
||||
: U : T
|
||||
|
||||
|
||||
/**
|
||||
* Is T a Record?
|
||||
*/
|
||||
type IsObject<T extends any> = T extends Array<any>
|
||||
? False
|
||||
: T extends Date
|
||||
? False
|
||||
: T extends Uint8Array
|
||||
? False
|
||||
: T extends BigInt
|
||||
? False
|
||||
: T extends object
|
||||
? True
|
||||
: False
|
||||
|
||||
|
||||
/**
|
||||
* If it's T[], return T
|
||||
*/
|
||||
export type UnEnumerate<T extends unknown> = T extends Array<infer U> ? U : T
|
||||
|
||||
/**
|
||||
* From ts-toolbelt
|
||||
*/
|
||||
|
||||
type __Either<O extends object, K extends Key> = Omit<O, K> &
|
||||
{
|
||||
// Merge all but K
|
||||
[P in K]: Prisma__Pick<O, P & keyof O> // With K possibilities
|
||||
}[K]
|
||||
|
||||
type EitherStrict<O extends object, K extends Key> = Strict<__Either<O, K>>
|
||||
|
||||
type EitherLoose<O extends object, K extends Key> = ComputeRaw<__Either<O, K>>
|
||||
|
||||
type _Either<
|
||||
O extends object,
|
||||
K extends Key,
|
||||
strict extends Boolean
|
||||
> = {
|
||||
1: EitherStrict<O, K>
|
||||
0: EitherLoose<O, K>
|
||||
}[strict]
|
||||
|
||||
export type Either<
|
||||
O extends object,
|
||||
K extends Key,
|
||||
strict extends Boolean = 1
|
||||
> = O extends unknown ? _Either<O, K, strict> : never
|
||||
|
||||
export type Union = any
|
||||
|
||||
export type PatchUndefined<O extends object, O1 extends object> = {
|
||||
[K in keyof O]: O[K] extends undefined ? At<O1, K> : O[K]
|
||||
} & {}
|
||||
|
||||
/** Helper Types for "Merge" **/
|
||||
export type IntersectOf<U extends Union> = (
|
||||
U extends unknown ? (k: U) => void : never
|
||||
) extends (k: infer I) => void
|
||||
? I
|
||||
: never
|
||||
|
||||
export type Overwrite<O extends object, O1 extends object> = {
|
||||
[K in keyof O]: K extends keyof O1 ? O1[K] : O[K];
|
||||
} & {};
|
||||
|
||||
type _Merge<U extends object> = IntersectOf<Overwrite<U, {
|
||||
[K in keyof U]-?: At<U, K>;
|
||||
}>>;
|
||||
|
||||
type Key = string | number | symbol;
|
||||
type AtStrict<O extends object, K extends Key> = O[K & keyof O];
|
||||
type AtLoose<O extends object, K extends Key> = O extends unknown ? AtStrict<O, K> : never;
|
||||
export type At<O extends object, K extends Key, strict extends Boolean = 1> = {
|
||||
1: AtStrict<O, K>;
|
||||
0: AtLoose<O, K>;
|
||||
}[strict];
|
||||
|
||||
export type ComputeRaw<A extends any> = A extends Function ? A : {
|
||||
[K in keyof A]: A[K];
|
||||
} & {};
|
||||
|
||||
export type OptionalFlat<O> = {
|
||||
[K in keyof O]?: O[K];
|
||||
} & {};
|
||||
|
||||
type _Record<K extends keyof any, T> = {
|
||||
[P in K]: T;
|
||||
};
|
||||
|
||||
// cause typescript not to expand types and preserve names
|
||||
type NoExpand<T> = T extends unknown ? T : never;
|
||||
|
||||
// this type assumes the passed object is entirely optional
|
||||
export type AtLeast<O extends object, K extends string> = NoExpand<
|
||||
O extends unknown
|
||||
? | (K extends keyof O ? { [P in K]: O[P] } & O : O)
|
||||
| {[P in keyof O as P extends K ? P : never]-?: O[P]} & O
|
||||
: never>;
|
||||
|
||||
type _Strict<U, _U = U> = U extends unknown ? U & OptionalFlat<_Record<Exclude<Keys<_U>, keyof U>, never>> : never;
|
||||
|
||||
export type Strict<U extends object> = ComputeRaw<_Strict<U>>;
|
||||
/** End Helper Types for "Merge" **/
|
||||
|
||||
export type Merge<U extends object> = ComputeRaw<_Merge<Strict<U>>>;
|
||||
|
||||
export type Boolean = True | False
|
||||
|
||||
export type True = 1
|
||||
|
||||
export type False = 0
|
||||
|
||||
export type Not<B extends Boolean> = {
|
||||
0: 1
|
||||
1: 0
|
||||
}[B]
|
||||
|
||||
export type Extends<A1 extends any, A2 extends any> = [A1] extends [never]
|
||||
? 0 // anything `never` is false
|
||||
: A1 extends A2
|
||||
? 1
|
||||
: 0
|
||||
|
||||
export type Has<U extends Union, U1 extends Union> = Not<
|
||||
Extends<Exclude<U1, U>, U1>
|
||||
>
|
||||
|
||||
export type Or<B1 extends Boolean, B2 extends Boolean> = {
|
||||
0: {
|
||||
0: 0
|
||||
1: 1
|
||||
}
|
||||
1: {
|
||||
0: 1
|
||||
1: 1
|
||||
}
|
||||
}[B1][B2]
|
||||
|
||||
export type Keys<U extends Union> = U extends unknown ? keyof U : never
|
||||
|
||||
export type GetScalarType<T, O> = O extends object ? {
|
||||
[P in keyof T]: P extends keyof O
|
||||
? O[P]
|
||||
: never
|
||||
} : never
|
||||
|
||||
type FieldPaths<
|
||||
T,
|
||||
U = Omit<T, '_avg' | '_sum' | '_count' | '_min' | '_max'>
|
||||
> = IsObject<T> extends True ? U : T
|
||||
|
||||
export type GetHavingFields<T> = {
|
||||
[K in keyof T]: Or<
|
||||
Or<Extends<'OR', K>, Extends<'AND', K>>,
|
||||
Extends<'NOT', K>
|
||||
> extends True
|
||||
? // infer is only needed to not hit TS limit
|
||||
// based on the brilliant idea of Pierre-Antoine Mills
|
||||
// https://github.com/microsoft/TypeScript/issues/30188#issuecomment-478938437
|
||||
T[K] extends infer TK
|
||||
? GetHavingFields<UnEnumerate<TK> extends object ? Merge<UnEnumerate<TK>> : never>
|
||||
: never
|
||||
: {} extends FieldPaths<T[K]>
|
||||
? never
|
||||
: K
|
||||
}[keyof T]
|
||||
|
||||
/**
|
||||
* Convert tuple to union
|
||||
*/
|
||||
type _TupleToUnion<T> = T extends (infer E)[] ? E : never
|
||||
type TupleToUnion<K extends readonly any[]> = _TupleToUnion<K>
|
||||
export type MaybeTupleToUnion<T> = T extends any[] ? TupleToUnion<T> : T
|
||||
|
||||
/**
|
||||
* Like `Pick`, but additionally can also accept an array of keys
|
||||
*/
|
||||
export type PickEnumerable<T, K extends Enumerable<keyof T> | keyof T> = Prisma__Pick<T, MaybeTupleToUnion<K>>
|
||||
|
||||
/**
|
||||
* Exclude all keys with underscores
|
||||
*/
|
||||
export type ExcludeUnderscoreKeys<T extends string> = T extends `_${string}` ? never : T
|
||||
|
||||
|
||||
export type FieldRef<Model, FieldType> = runtime.FieldRef<Model, FieldType>
|
||||
|
||||
type FieldRefInputType<Model, FieldType> = Model extends never ? never : FieldRef<Model, FieldType>
|
||||
|
||||
|
||||
export const ModelName = {
|
||||
activity_logs: 'activity_logs',
|
||||
training_menus: 'training_menus',
|
||||
user_recaps: 'user_recaps'
|
||||
} as const
|
||||
|
||||
export type ModelName = (typeof ModelName)[keyof typeof ModelName]
|
||||
|
||||
|
||||
|
||||
export interface TypeMapCb<GlobalOmitOptions = {}> extends runtime.Types.Utils.Fn<{extArgs: runtime.Types.Extensions.InternalArgs }, runtime.Types.Utils.Record<string, any>> {
|
||||
returns: TypeMap<this['params']['extArgs'], GlobalOmitOptions>
|
||||
}
|
||||
|
||||
export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs, GlobalOmitOptions = {}> = {
|
||||
globalOmitOptions: {
|
||||
omit: GlobalOmitOptions
|
||||
}
|
||||
meta: {
|
||||
modelProps: "activity_logs" | "training_menus" | "user_recaps"
|
||||
txIsolationLevel: TransactionIsolationLevel
|
||||
}
|
||||
model: {
|
||||
activity_logs: {
|
||||
payload: Prisma.$activity_logsPayload<ExtArgs>
|
||||
fields: Prisma.activity_logsFieldRefs
|
||||
operations: {
|
||||
findUnique: {
|
||||
args: Prisma.activity_logsFindUniqueArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$activity_logsPayload> | null
|
||||
}
|
||||
findUniqueOrThrow: {
|
||||
args: Prisma.activity_logsFindUniqueOrThrowArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$activity_logsPayload>
|
||||
}
|
||||
findFirst: {
|
||||
args: Prisma.activity_logsFindFirstArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$activity_logsPayload> | null
|
||||
}
|
||||
findFirstOrThrow: {
|
||||
args: Prisma.activity_logsFindFirstOrThrowArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$activity_logsPayload>
|
||||
}
|
||||
findMany: {
|
||||
args: Prisma.activity_logsFindManyArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$activity_logsPayload>[]
|
||||
}
|
||||
create: {
|
||||
args: Prisma.activity_logsCreateArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$activity_logsPayload>
|
||||
}
|
||||
createMany: {
|
||||
args: Prisma.activity_logsCreateManyArgs<ExtArgs>
|
||||
result: BatchPayload
|
||||
}
|
||||
createManyAndReturn: {
|
||||
args: Prisma.activity_logsCreateManyAndReturnArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$activity_logsPayload>[]
|
||||
}
|
||||
delete: {
|
||||
args: Prisma.activity_logsDeleteArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$activity_logsPayload>
|
||||
}
|
||||
update: {
|
||||
args: Prisma.activity_logsUpdateArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$activity_logsPayload>
|
||||
}
|
||||
deleteMany: {
|
||||
args: Prisma.activity_logsDeleteManyArgs<ExtArgs>
|
||||
result: BatchPayload
|
||||
}
|
||||
updateMany: {
|
||||
args: Prisma.activity_logsUpdateManyArgs<ExtArgs>
|
||||
result: BatchPayload
|
||||
}
|
||||
updateManyAndReturn: {
|
||||
args: Prisma.activity_logsUpdateManyAndReturnArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$activity_logsPayload>[]
|
||||
}
|
||||
upsert: {
|
||||
args: Prisma.activity_logsUpsertArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$activity_logsPayload>
|
||||
}
|
||||
aggregate: {
|
||||
args: Prisma.Activity_logsAggregateArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.Optional<Prisma.AggregateActivity_logs>
|
||||
}
|
||||
groupBy: {
|
||||
args: Prisma.activity_logsGroupByArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.Optional<Prisma.Activity_logsGroupByOutputType>[]
|
||||
}
|
||||
count: {
|
||||
args: Prisma.activity_logsCountArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.Optional<Prisma.Activity_logsCountAggregateOutputType> | number
|
||||
}
|
||||
}
|
||||
}
|
||||
training_menus: {
|
||||
payload: Prisma.$training_menusPayload<ExtArgs>
|
||||
fields: Prisma.training_menusFieldRefs
|
||||
operations: {
|
||||
findUnique: {
|
||||
args: Prisma.training_menusFindUniqueArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$training_menusPayload> | null
|
||||
}
|
||||
findUniqueOrThrow: {
|
||||
args: Prisma.training_menusFindUniqueOrThrowArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$training_menusPayload>
|
||||
}
|
||||
findFirst: {
|
||||
args: Prisma.training_menusFindFirstArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$training_menusPayload> | null
|
||||
}
|
||||
findFirstOrThrow: {
|
||||
args: Prisma.training_menusFindFirstOrThrowArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$training_menusPayload>
|
||||
}
|
||||
findMany: {
|
||||
args: Prisma.training_menusFindManyArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$training_menusPayload>[]
|
||||
}
|
||||
create: {
|
||||
args: Prisma.training_menusCreateArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$training_menusPayload>
|
||||
}
|
||||
createMany: {
|
||||
args: Prisma.training_menusCreateManyArgs<ExtArgs>
|
||||
result: BatchPayload
|
||||
}
|
||||
createManyAndReturn: {
|
||||
args: Prisma.training_menusCreateManyAndReturnArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$training_menusPayload>[]
|
||||
}
|
||||
delete: {
|
||||
args: Prisma.training_menusDeleteArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$training_menusPayload>
|
||||
}
|
||||
update: {
|
||||
args: Prisma.training_menusUpdateArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$training_menusPayload>
|
||||
}
|
||||
deleteMany: {
|
||||
args: Prisma.training_menusDeleteManyArgs<ExtArgs>
|
||||
result: BatchPayload
|
||||
}
|
||||
updateMany: {
|
||||
args: Prisma.training_menusUpdateManyArgs<ExtArgs>
|
||||
result: BatchPayload
|
||||
}
|
||||
updateManyAndReturn: {
|
||||
args: Prisma.training_menusUpdateManyAndReturnArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$training_menusPayload>[]
|
||||
}
|
||||
upsert: {
|
||||
args: Prisma.training_menusUpsertArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$training_menusPayload>
|
||||
}
|
||||
aggregate: {
|
||||
args: Prisma.Training_menusAggregateArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.Optional<Prisma.AggregateTraining_menus>
|
||||
}
|
||||
groupBy: {
|
||||
args: Prisma.training_menusGroupByArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.Optional<Prisma.Training_menusGroupByOutputType>[]
|
||||
}
|
||||
count: {
|
||||
args: Prisma.training_menusCountArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.Optional<Prisma.Training_menusCountAggregateOutputType> | number
|
||||
}
|
||||
}
|
||||
}
|
||||
user_recaps: {
|
||||
payload: Prisma.$user_recapsPayload<ExtArgs>
|
||||
fields: Prisma.user_recapsFieldRefs
|
||||
operations: {
|
||||
findUnique: {
|
||||
args: Prisma.user_recapsFindUniqueArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$user_recapsPayload> | null
|
||||
}
|
||||
findUniqueOrThrow: {
|
||||
args: Prisma.user_recapsFindUniqueOrThrowArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$user_recapsPayload>
|
||||
}
|
||||
findFirst: {
|
||||
args: Prisma.user_recapsFindFirstArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$user_recapsPayload> | null
|
||||
}
|
||||
findFirstOrThrow: {
|
||||
args: Prisma.user_recapsFindFirstOrThrowArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$user_recapsPayload>
|
||||
}
|
||||
findMany: {
|
||||
args: Prisma.user_recapsFindManyArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$user_recapsPayload>[]
|
||||
}
|
||||
create: {
|
||||
args: Prisma.user_recapsCreateArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$user_recapsPayload>
|
||||
}
|
||||
createMany: {
|
||||
args: Prisma.user_recapsCreateManyArgs<ExtArgs>
|
||||
result: BatchPayload
|
||||
}
|
||||
createManyAndReturn: {
|
||||
args: Prisma.user_recapsCreateManyAndReturnArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$user_recapsPayload>[]
|
||||
}
|
||||
delete: {
|
||||
args: Prisma.user_recapsDeleteArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$user_recapsPayload>
|
||||
}
|
||||
update: {
|
||||
args: Prisma.user_recapsUpdateArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$user_recapsPayload>
|
||||
}
|
||||
deleteMany: {
|
||||
args: Prisma.user_recapsDeleteManyArgs<ExtArgs>
|
||||
result: BatchPayload
|
||||
}
|
||||
updateMany: {
|
||||
args: Prisma.user_recapsUpdateManyArgs<ExtArgs>
|
||||
result: BatchPayload
|
||||
}
|
||||
updateManyAndReturn: {
|
||||
args: Prisma.user_recapsUpdateManyAndReturnArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$user_recapsPayload>[]
|
||||
}
|
||||
upsert: {
|
||||
args: Prisma.user_recapsUpsertArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$user_recapsPayload>
|
||||
}
|
||||
aggregate: {
|
||||
args: Prisma.User_recapsAggregateArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.Optional<Prisma.AggregateUser_recaps>
|
||||
}
|
||||
groupBy: {
|
||||
args: Prisma.user_recapsGroupByArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.Optional<Prisma.User_recapsGroupByOutputType>[]
|
||||
}
|
||||
count: {
|
||||
args: Prisma.user_recapsCountArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.Optional<Prisma.User_recapsCountAggregateOutputType> | number
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} & {
|
||||
other: {
|
||||
payload: any
|
||||
operations: {
|
||||
$executeRaw: {
|
||||
args: [query: TemplateStringsArray | Sql, ...values: any[]],
|
||||
result: any
|
||||
}
|
||||
$executeRawUnsafe: {
|
||||
args: [query: string, ...values: any[]],
|
||||
result: any
|
||||
}
|
||||
$queryRaw: {
|
||||
args: [query: TemplateStringsArray | Sql, ...values: any[]],
|
||||
result: any
|
||||
}
|
||||
$queryRawUnsafe: {
|
||||
args: [query: string, ...values: any[]],
|
||||
result: any
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enums
|
||||
*/
|
||||
|
||||
export const TransactionIsolationLevel = runtime.makeStrictEnum({
|
||||
ReadUncommitted: 'ReadUncommitted',
|
||||
ReadCommitted: 'ReadCommitted',
|
||||
RepeatableRead: 'RepeatableRead',
|
||||
Serializable: 'Serializable'
|
||||
} as const)
|
||||
|
||||
export type TransactionIsolationLevel = (typeof TransactionIsolationLevel)[keyof typeof TransactionIsolationLevel]
|
||||
|
||||
|
||||
export const Activity_logsScalarFieldEnum = {
|
||||
id: 'id',
|
||||
timestamp: 'timestamp',
|
||||
status: 'status',
|
||||
confidence: 'confidence',
|
||||
details: 'details'
|
||||
} as const
|
||||
|
||||
export type Activity_logsScalarFieldEnum = (typeof Activity_logsScalarFieldEnum)[keyof typeof Activity_logsScalarFieldEnum]
|
||||
|
||||
|
||||
export const Training_menusScalarFieldEnum = {
|
||||
id: 'id',
|
||||
name: 'name',
|
||||
exercises: 'exercises',
|
||||
created_at: 'created_at'
|
||||
} as const
|
||||
|
||||
export type Training_menusScalarFieldEnum = (typeof Training_menusScalarFieldEnum)[keyof typeof Training_menusScalarFieldEnum]
|
||||
|
||||
|
||||
export const User_recapsScalarFieldEnum = {
|
||||
id: 'id',
|
||||
menu_id: 'menu_id',
|
||||
summary: 'summary',
|
||||
completed_at: 'completed_at'
|
||||
} as const
|
||||
|
||||
export type User_recapsScalarFieldEnum = (typeof User_recapsScalarFieldEnum)[keyof typeof User_recapsScalarFieldEnum]
|
||||
|
||||
|
||||
export const SortOrder = {
|
||||
asc: 'asc',
|
||||
desc: 'desc'
|
||||
} as const
|
||||
|
||||
export type SortOrder = (typeof SortOrder)[keyof typeof SortOrder]
|
||||
|
||||
|
||||
export const NullableJsonNullValueInput = {
|
||||
DbNull: DbNull,
|
||||
JsonNull: JsonNull
|
||||
} as const
|
||||
|
||||
export type NullableJsonNullValueInput = (typeof NullableJsonNullValueInput)[keyof typeof NullableJsonNullValueInput]
|
||||
|
||||
|
||||
export const QueryMode = {
|
||||
default: 'default',
|
||||
insensitive: 'insensitive'
|
||||
} as const
|
||||
|
||||
export type QueryMode = (typeof QueryMode)[keyof typeof QueryMode]
|
||||
|
||||
|
||||
export const JsonNullValueFilter = {
|
||||
DbNull: DbNull,
|
||||
JsonNull: JsonNull,
|
||||
AnyNull: AnyNull
|
||||
} as const
|
||||
|
||||
export type JsonNullValueFilter = (typeof JsonNullValueFilter)[keyof typeof JsonNullValueFilter]
|
||||
|
||||
|
||||
export const NullsOrder = {
|
||||
first: 'first',
|
||||
last: 'last'
|
||||
} as const
|
||||
|
||||
export type NullsOrder = (typeof NullsOrder)[keyof typeof NullsOrder]
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Field references
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Reference to a field of type 'Int'
|
||||
*/
|
||||
export type IntFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Int'>
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Reference to a field of type 'Int[]'
|
||||
*/
|
||||
export type ListIntFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Int[]'>
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Reference to a field of type 'DateTime'
|
||||
*/
|
||||
export type DateTimeFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'DateTime'>
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Reference to a field of type 'DateTime[]'
|
||||
*/
|
||||
export type ListDateTimeFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'DateTime[]'>
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Reference to a field of type 'String'
|
||||
*/
|
||||
export type StringFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'String'>
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Reference to a field of type 'String[]'
|
||||
*/
|
||||
export type ListStringFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'String[]'>
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Reference to a field of type 'Json'
|
||||
*/
|
||||
export type JsonFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Json'>
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Reference to a field of type 'QueryMode'
|
||||
*/
|
||||
export type EnumQueryModeFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'QueryMode'>
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Reference to a field of type 'Float'
|
||||
*/
|
||||
export type FloatFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Float'>
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Reference to a field of type 'Float[]'
|
||||
*/
|
||||
export type ListFloatFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Float[]'>
|
||||
|
||||
|
||||
/**
|
||||
* Batch Payload for updateMany & deleteMany & createMany
|
||||
*/
|
||||
export type BatchPayload = {
|
||||
count: number
|
||||
}
|
||||
|
||||
export const defineExtension = runtime.Extensions.defineExtension as unknown as runtime.Types.Extensions.ExtendsHook<"define", TypeMapCb, runtime.Types.Extensions.DefaultArgs>
|
||||
export type DefaultPrismaClient = PrismaClient
|
||||
export type ErrorFormat = 'pretty' | 'colorless' | 'minimal'
|
||||
export type PrismaClientOptions = ({
|
||||
/**
|
||||
* Instance of a Driver Adapter, e.g., like one provided by `@prisma/adapter-pg`.
|
||||
*/
|
||||
adapter: runtime.SqlDriverAdapterFactory
|
||||
accelerateUrl?: never
|
||||
} | {
|
||||
/**
|
||||
* Prisma Accelerate URL allowing the client to connect through Accelerate instead of a direct database.
|
||||
*/
|
||||
accelerateUrl: string
|
||||
adapter?: never
|
||||
}) & {
|
||||
/**
|
||||
* @default "colorless"
|
||||
*/
|
||||
errorFormat?: ErrorFormat
|
||||
/**
|
||||
* @example
|
||||
* ```
|
||||
* // Shorthand for `emit: 'stdout'`
|
||||
* log: ['query', 'info', 'warn', 'error']
|
||||
*
|
||||
* // Emit as events only
|
||||
* log: [
|
||||
* { emit: 'event', level: 'query' },
|
||||
* { emit: 'event', level: 'info' },
|
||||
* { emit: 'event', level: 'warn' }
|
||||
* { emit: 'event', level: 'error' }
|
||||
* ]
|
||||
*
|
||||
* / Emit as events and log to stdout
|
||||
* og: [
|
||||
* { emit: 'stdout', level: 'query' },
|
||||
* { emit: 'stdout', level: 'info' },
|
||||
* { emit: 'stdout', level: 'warn' }
|
||||
* { emit: 'stdout', level: 'error' }
|
||||
*
|
||||
* ```
|
||||
* Read more in our [docs](https://pris.ly/d/logging).
|
||||
*/
|
||||
log?: (LogLevel | LogDefinition)[]
|
||||
/**
|
||||
* The default values for transactionOptions
|
||||
* maxWait ?= 2000
|
||||
* timeout ?= 5000
|
||||
*/
|
||||
transactionOptions?: {
|
||||
maxWait?: number
|
||||
timeout?: number
|
||||
isolationLevel?: TransactionIsolationLevel
|
||||
}
|
||||
/**
|
||||
* Global configuration for omitting model fields by default.
|
||||
*
|
||||
* @example
|
||||
* ```
|
||||
* const prisma = new PrismaClient({
|
||||
* omit: {
|
||||
* user: {
|
||||
* password: true
|
||||
* }
|
||||
* }
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
omit?: GlobalOmitConfig
|
||||
/**
|
||||
* SQL commenter plugins that add metadata to SQL queries as comments.
|
||||
* Comments follow the sqlcommenter format: https://google.github.io/sqlcommenter/
|
||||
*
|
||||
* @example
|
||||
* ```
|
||||
* const prisma = new PrismaClient({
|
||||
* adapter,
|
||||
* comments: [
|
||||
* traceContext(),
|
||||
* queryInsights(),
|
||||
* ],
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
comments?: runtime.SqlCommenterPlugin[]
|
||||
}
|
||||
export type GlobalOmitConfig = {
|
||||
activity_logs?: Prisma.activity_logsOmit
|
||||
training_menus?: Prisma.training_menusOmit
|
||||
user_recaps?: Prisma.user_recapsOmit
|
||||
}
|
||||
|
||||
/* Types for Logging */
|
||||
export type LogLevel = 'info' | 'query' | 'warn' | 'error'
|
||||
export type LogDefinition = {
|
||||
level: LogLevel
|
||||
emit: 'stdout' | 'event'
|
||||
}
|
||||
|
||||
export type CheckIsLogLevel<T> = T extends LogLevel ? T : never;
|
||||
|
||||
export type GetLogType<T> = CheckIsLogLevel<
|
||||
T extends LogDefinition ? T['level'] : T
|
||||
>;
|
||||
|
||||
export type GetEvents<T extends any[]> = T extends Array<LogLevel | LogDefinition>
|
||||
? GetLogType<T[number]>
|
||||
: never;
|
||||
|
||||
export type QueryEvent = {
|
||||
timestamp: Date
|
||||
query: string
|
||||
params: string
|
||||
duration: number
|
||||
target: string
|
||||
}
|
||||
|
||||
export type LogEvent = {
|
||||
timestamp: Date
|
||||
message: string
|
||||
target: string
|
||||
}
|
||||
/* End Types for Logging */
|
||||
|
||||
|
||||
export type PrismaAction =
|
||||
| 'findUnique'
|
||||
| 'findUniqueOrThrow'
|
||||
| 'findMany'
|
||||
| 'findFirst'
|
||||
| 'findFirstOrThrow'
|
||||
| 'create'
|
||||
| 'createMany'
|
||||
| 'createManyAndReturn'
|
||||
| 'update'
|
||||
| 'updateMany'
|
||||
| 'updateManyAndReturn'
|
||||
| 'upsert'
|
||||
| 'delete'
|
||||
| 'deleteMany'
|
||||
| 'executeRaw'
|
||||
| 'queryRaw'
|
||||
| 'aggregate'
|
||||
| 'count'
|
||||
| 'runCommandRaw'
|
||||
| 'findRaw'
|
||||
| 'groupBy'
|
||||
|
||||
/**
|
||||
* `PrismaClient` proxy available in interactive transactions.
|
||||
*/
|
||||
export type TransactionClient = Omit<DefaultPrismaClient, runtime.ITXClientDenyList>
|
||||
|
||||
145
lib/prisma/client/internal/prismaNamespaceBrowser.ts
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
// @ts-nocheck
|
||||
/*
|
||||
* WARNING: This is an internal file that is subject to change!
|
||||
*
|
||||
* 🛑 Under no circumstances should you import this file directly! 🛑
|
||||
*
|
||||
* All exports from this file are wrapped under a `Prisma` namespace object in the browser.ts file.
|
||||
* While this enables partial backward compatibility, it is not part of the stable public API.
|
||||
*
|
||||
* If you are looking for your Models, Enums, and Input Types, please import them from the respective
|
||||
* model files in the `model` directory!
|
||||
*/
|
||||
|
||||
import * as runtime from "@prisma/client/runtime/index-browser"
|
||||
|
||||
export type * from '../models'
|
||||
export type * from './prismaNamespace'
|
||||
|
||||
export const Decimal = runtime.Decimal
|
||||
|
||||
|
||||
export const NullTypes = {
|
||||
DbNull: runtime.NullTypes.DbNull as (new (secret: never) => typeof runtime.DbNull),
|
||||
JsonNull: runtime.NullTypes.JsonNull as (new (secret: never) => typeof runtime.JsonNull),
|
||||
AnyNull: runtime.NullTypes.AnyNull as (new (secret: never) => typeof runtime.AnyNull),
|
||||
}
|
||||
/**
|
||||
* Helper for filtering JSON entries that have `null` on the database (empty on the db)
|
||||
*
|
||||
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
|
||||
*/
|
||||
export const DbNull = runtime.DbNull
|
||||
|
||||
/**
|
||||
* Helper for filtering JSON entries that have JSON `null` values (not empty on the db)
|
||||
*
|
||||
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
|
||||
*/
|
||||
export const JsonNull = runtime.JsonNull
|
||||
|
||||
/**
|
||||
* Helper for filtering JSON entries that are `Prisma.DbNull` or `Prisma.JsonNull`
|
||||
*
|
||||
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
|
||||
*/
|
||||
export const AnyNull = runtime.AnyNull
|
||||
|
||||
|
||||
export const ModelName = {
|
||||
activity_logs: 'activity_logs',
|
||||
training_menus: 'training_menus',
|
||||
user_recaps: 'user_recaps'
|
||||
} as const
|
||||
|
||||
export type ModelName = (typeof ModelName)[keyof typeof ModelName]
|
||||
|
||||
/*
|
||||
* Enums
|
||||
*/
|
||||
|
||||
export const TransactionIsolationLevel = {
|
||||
ReadUncommitted: 'ReadUncommitted',
|
||||
ReadCommitted: 'ReadCommitted',
|
||||
RepeatableRead: 'RepeatableRead',
|
||||
Serializable: 'Serializable'
|
||||
} as const
|
||||
|
||||
export type TransactionIsolationLevel = (typeof TransactionIsolationLevel)[keyof typeof TransactionIsolationLevel]
|
||||
|
||||
|
||||
export const Activity_logsScalarFieldEnum = {
|
||||
id: 'id',
|
||||
timestamp: 'timestamp',
|
||||
status: 'status',
|
||||
confidence: 'confidence',
|
||||
details: 'details'
|
||||
} as const
|
||||
|
||||
export type Activity_logsScalarFieldEnum = (typeof Activity_logsScalarFieldEnum)[keyof typeof Activity_logsScalarFieldEnum]
|
||||
|
||||
|
||||
export const Training_menusScalarFieldEnum = {
|
||||
id: 'id',
|
||||
name: 'name',
|
||||
exercises: 'exercises',
|
||||
created_at: 'created_at'
|
||||
} as const
|
||||
|
||||
export type Training_menusScalarFieldEnum = (typeof Training_menusScalarFieldEnum)[keyof typeof Training_menusScalarFieldEnum]
|
||||
|
||||
|
||||
export const User_recapsScalarFieldEnum = {
|
||||
id: 'id',
|
||||
menu_id: 'menu_id',
|
||||
summary: 'summary',
|
||||
completed_at: 'completed_at'
|
||||
} as const
|
||||
|
||||
export type User_recapsScalarFieldEnum = (typeof User_recapsScalarFieldEnum)[keyof typeof User_recapsScalarFieldEnum]
|
||||
|
||||
|
||||
export const SortOrder = {
|
||||
asc: 'asc',
|
||||
desc: 'desc'
|
||||
} as const
|
||||
|
||||
export type SortOrder = (typeof SortOrder)[keyof typeof SortOrder]
|
||||
|
||||
|
||||
export const NullableJsonNullValueInput = {
|
||||
DbNull: 'DbNull',
|
||||
JsonNull: 'JsonNull'
|
||||
} as const
|
||||
|
||||
export type NullableJsonNullValueInput = (typeof NullableJsonNullValueInput)[keyof typeof NullableJsonNullValueInput]
|
||||
|
||||
|
||||
export const QueryMode = {
|
||||
default: 'default',
|
||||
insensitive: 'insensitive'
|
||||
} as const
|
||||
|
||||
export type QueryMode = (typeof QueryMode)[keyof typeof QueryMode]
|
||||
|
||||
|
||||
export const JsonNullValueFilter = {
|
||||
DbNull: 'DbNull',
|
||||
JsonNull: 'JsonNull',
|
||||
AnyNull: 'AnyNull'
|
||||
} as const
|
||||
|
||||
export type JsonNullValueFilter = (typeof JsonNullValueFilter)[keyof typeof JsonNullValueFilter]
|
||||
|
||||
|
||||
export const NullsOrder = {
|
||||
first: 'first',
|
||||
last: 'last'
|
||||
} as const
|
||||
|
||||
export type NullsOrder = (typeof NullsOrder)[keyof typeof NullsOrder]
|
||||
|
||||
14
lib/prisma/client/models.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
// @ts-nocheck
|
||||
/*
|
||||
* This is a barrel export file for all models and their related types.
|
||||
*
|
||||
* 🟢 You can import this file directly.
|
||||
*/
|
||||
export type * from './models/activity_logs'
|
||||
export type * from './models/training_menus'
|
||||
export type * from './models/user_recaps'
|
||||
export type * from './commonInputTypes'
|
||||
1195
lib/prisma/client/models/activity_logs.ts
Normal file
1325
lib/prisma/client/models/training_menus.ts
Normal file
1371
lib/prisma/client/models/user_recaps.ts
Normal file
32
next.config.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
|
||||
const nextConfig = {
|
||||
// Transpile the detector wrapper (ESM) but let @mediapipe/pose be handled by alias or externals
|
||||
transpilePackages: ['@tensorflow-models/pose-detection', '@/app/generated/client'],
|
||||
experimental: {
|
||||
esmExternals: "loose", // Allow mixing CJS/ESM
|
||||
},
|
||||
webpack: (config: any) => {
|
||||
// Alias @mediapipe/pose to our shim which expects window.Pose
|
||||
const path = require('path');
|
||||
config.resolve.alias['@mediapipe/pose'] = path.resolve(__dirname, 'lib/mediapipe-shim.js');
|
||||
|
||||
config.resolve.extensionAlias = {
|
||||
'.js': ['.ts', '.tsx', '.js', '.jsx'],
|
||||
};
|
||||
return config;
|
||||
},
|
||||
async headers() {
|
||||
return [
|
||||
{
|
||||
source: "/api/:path*",
|
||||
headers: [
|
||||
{ key: "Access-Control-Allow-Origin", value: "*" },
|
||||
{ key: "Access-Control-Allow-Methods", value: "GET, POST, PUT, DELETE, OPTIONS" },
|
||||
{ key: "Access-Control-Allow-Headers", value: "Content-Type, Authorization" },
|
||||
]
|
||||
}
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
8377
package-lock.json
generated
Normal file
48
package.json
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
{
|
||||
"name": "straps-dev",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "concurrently \"next dev --webpack\" \"prisma studio\"",
|
||||
"build": "prisma generate && next build --webpack",
|
||||
"start": "next start",
|
||||
"lint": "eslint",
|
||||
"db:studio": "prisma studio"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mediapipe/pose": "^0.5.1675469404",
|
||||
"@mediapipe/tasks-vision": "^0.10.22-rc.20250304",
|
||||
"@prisma/client": "^6.19.1",
|
||||
"@tensorflow-models/pose-detection": "^2.1.3",
|
||||
"@tensorflow/tfjs": "^4.22.0",
|
||||
"@tensorflow/tfjs-backend-webgl": "^4.22.0",
|
||||
"axios": "^1.13.2",
|
||||
"bcryptjs": "^3.0.3",
|
||||
"clsx": "^2.1.1",
|
||||
"framer-motion": "^12.23.26",
|
||||
"lucide-react": "^0.562.0",
|
||||
"next": "^16.1.1",
|
||||
"next-auth": "^5.0.0-beta.30",
|
||||
"prisma": "^6.19.1",
|
||||
"react": "^19.2.3",
|
||||
"react-dom": "^19.2.3",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"zod": "^4.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"concurrently": "^9.2.1",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "16.1.0",
|
||||
"tailwindcss": "^4",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "^5"
|
||||
},
|
||||
"prisma": {
|
||||
"seed": "npx tsx prisma/seed.ts"
|
||||
}
|
||||
}
|
||||
7
postcss.config.mjs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
const config = {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
18
prisma.config.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
// This file was generated by Prisma, and assumes you have installed the following:
|
||||
// npm install --save-dev prisma dotenv
|
||||
import "dotenv/config";
|
||||
import { defineConfig } from "prisma/config";
|
||||
|
||||
export default defineConfig({
|
||||
schema: "prisma/schema.prisma",
|
||||
migrations: {
|
||||
path: "prisma/migrations",
|
||||
},
|
||||
datasource: {
|
||||
url: process.env["DATABASE_URL"]!,
|
||||
},
|
||||
// @ts-expect-error - seed is valid at runtime but missing from type definition in this version
|
||||
seed: {
|
||||
command: "npx tsx prisma/seed.ts",
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
-- CreateTable
|
||||
CREATE TABLE "users" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" VARCHAR NOT NULL,
|
||||
"role" VARCHAR NOT NULL,
|
||||
"coach_id" TEXT,
|
||||
"created_at" TIMESTAMP(6) DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "activity_logs" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"timestamp" TIMESTAMP(6),
|
||||
"status" VARCHAR,
|
||||
"confidence" VARCHAR,
|
||||
"details" JSONB,
|
||||
"user_id" TEXT,
|
||||
|
||||
CONSTRAINT "activity_logs_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "training_menus" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"name" VARCHAR,
|
||||
"exercises" JSONB,
|
||||
"created_at" TIMESTAMP(6),
|
||||
"author_id" TEXT,
|
||||
"client_id" TEXT,
|
||||
|
||||
CONSTRAINT "training_menus_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "user_recaps" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"menu_id" INTEGER,
|
||||
"user_id" TEXT,
|
||||
"summary" JSONB,
|
||||
"completed_at" TIMESTAMP(6),
|
||||
|
||||
CONSTRAINT "user_recaps_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "ix_users_id" ON "users"("id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "ix_users_coach_id" ON "users"("coach_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "ix_activity_logs_id" ON "activity_logs"("id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "ix_training_menus_id" ON "training_menus"("id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "ix_training_menus_name" ON "training_menus"("name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "ix_training_menus_author_id" ON "training_menus"("author_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "ix_user_recaps_id" ON "user_recaps"("id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "ix_user_recaps_user_id" ON "user_recaps"("user_id");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "users" ADD CONSTRAINT "users_coach_id_fkey" FOREIGN KEY ("coach_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "activity_logs" ADD CONSTRAINT "activity_logs_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "training_menus" ADD CONSTRAINT "training_menus_author_id_fkey" FOREIGN KEY ("author_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE NO ACTION;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "training_menus" ADD CONSTRAINT "training_menus_client_id_fkey" FOREIGN KEY ("client_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE NO ACTION;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "user_recaps" ADD CONSTRAINT "user_recaps_menu_id_fkey" FOREIGN KEY ("menu_id") REFERENCES "training_menus"("id") ON DELETE NO ACTION ON UPDATE NO ACTION;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "user_recaps" ADD CONSTRAINT "user_recaps_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
-- This migration is skipped because its changes (adding client_id)
|
||||
-- were manually merged into the previous migration to fix type mismatches (Integer vs String IDs).
|
||||
-- This file is kept to preserve migration history order.
|
||||
14
prisma/migrations/20260401111824_gih_db/migration.sql
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
-- DropForeignKey
|
||||
ALTER TABLE "training_menus" DROP CONSTRAINT "training_menus_author_id_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "training_menus" DROP CONSTRAINT "training_menus_client_id_fkey";
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "ix_activity_logs_user_id" ON "activity_logs"("user_id");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "training_menus" ADD CONSTRAINT "training_menus_author_id_fkey" FOREIGN KEY ("author_id") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "training_menus" ADD CONSTRAINT "training_menus_client_id_fkey" FOREIGN KEY ("client_id") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION;
|
||||
3
prisma/migrations/migration_lock.toml
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (e.g., Git)
|
||||
provider = "postgresql"
|
||||
75
prisma/schema.prisma
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
generator client {
|
||||
provider = "prisma-client"
|
||||
output = "../app/generated/client"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
|
||||
model users {
|
||||
id String @id @default(cuid()) // We will override this with our custom generator in logic, but cuid as fallback
|
||||
name String @db.VarChar
|
||||
role String @db.VarChar // COACH, CLIENT
|
||||
coach_id String?
|
||||
created_at DateTime? @default(now()) @db.Timestamp(6)
|
||||
|
||||
// Relations
|
||||
coach users? @relation("CoachClients", fields: [coach_id], references: [id])
|
||||
clients users[] @relation("CoachClients")
|
||||
|
||||
created_menus training_menus[] @relation("CreatedMenus")
|
||||
assigned_menus training_menus[] @relation("AssignedMenus")
|
||||
recaps user_recaps[]
|
||||
activity_logs activity_logs[]
|
||||
|
||||
@@index([id], map: "ix_users_id")
|
||||
@@index([coach_id], map: "ix_users_coach_id")
|
||||
}
|
||||
|
||||
model activity_logs {
|
||||
id Int @id @default(autoincrement())
|
||||
timestamp DateTime? @db.Timestamp(6)
|
||||
status String? @db.VarChar
|
||||
confidence String? @db.VarChar
|
||||
details Json?
|
||||
user_id String?
|
||||
|
||||
user users? @relation(fields: [user_id], references: [id])
|
||||
|
||||
@@index([id], map: "ix_activity_logs_id")
|
||||
@@index([user_id], map: "ix_activity_logs_user_id")
|
||||
}
|
||||
|
||||
model training_menus {
|
||||
id Int @id @default(autoincrement())
|
||||
name String? @db.VarChar
|
||||
exercises Json?
|
||||
created_at DateTime? @db.Timestamp(6)
|
||||
author_id String?
|
||||
client_id String?
|
||||
|
||||
author users? @relation("CreatedMenus", fields: [author_id], references: [id], onDelete: NoAction, onUpdate: NoAction)
|
||||
assigned_client users? @relation("AssignedMenus", fields: [client_id], references: [id], onDelete: NoAction, onUpdate: NoAction)
|
||||
user_recaps user_recaps[]
|
||||
|
||||
@@index([id], map: "ix_training_menus_id")
|
||||
@@index([name], map: "ix_training_menus_name")
|
||||
@@index([author_id], map: "ix_training_menus_author_id")
|
||||
}
|
||||
|
||||
model user_recaps {
|
||||
id Int @id @default(autoincrement())
|
||||
menu_id Int?
|
||||
user_id String?
|
||||
summary Json?
|
||||
completed_at DateTime? @db.Timestamp(6)
|
||||
|
||||
training_menus training_menus? @relation(fields: [menu_id], references: [id], onDelete: NoAction, onUpdate: NoAction)
|
||||
user users? @relation(fields: [user_id], references: [id])
|
||||
|
||||
@@index([id], map: "ix_user_recaps_id")
|
||||
@@index([user_id], map: "ix_user_recaps_user_id")
|
||||
}
|
||||
79
prisma/seed.ts
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
|
||||
import 'dotenv/config';
|
||||
import { PrismaClient } from '../app/generated/client/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
console.log('Start seeding ...');
|
||||
|
||||
// Create Coach 1 (Will link to Client 1 & 2)
|
||||
const coach1 = await prisma.users.upsert({
|
||||
where: { id: "C00001" },
|
||||
update: {},
|
||||
create: {
|
||||
id: "C00001",
|
||||
name: 'Coach One',
|
||||
role: 'COACH',
|
||||
},
|
||||
});
|
||||
|
||||
// Create Coach 2 (Will link to Client 3)
|
||||
const coach2 = await prisma.users.upsert({
|
||||
where: { id: "C00002" },
|
||||
update: {},
|
||||
create: {
|
||||
id: "C00002",
|
||||
name: 'Coach Two',
|
||||
role: 'COACH',
|
||||
},
|
||||
});
|
||||
|
||||
// Create Client 1 (Linked to Coach 1)
|
||||
const client1 = await prisma.users.upsert({
|
||||
where: { id: "U00001" },
|
||||
update: {},
|
||||
create: {
|
||||
id: "U00001",
|
||||
name: 'Client One',
|
||||
role: 'CLIENT',
|
||||
coach_id: coach1.id,
|
||||
},
|
||||
});
|
||||
|
||||
// Create Client 2 (Linked to Coach 1)
|
||||
const client2 = await prisma.users.upsert({
|
||||
where: { id: "U00002" },
|
||||
update: {},
|
||||
create: {
|
||||
id: "U00002",
|
||||
name: 'Client Two',
|
||||
role: 'CLIENT',
|
||||
coach_id: coach1.id,
|
||||
},
|
||||
});
|
||||
|
||||
// Create Client 3 (Linked to Coach 2)
|
||||
const client3 = await prisma.users.upsert({
|
||||
where: { id: "U00003" },
|
||||
update: {},
|
||||
create: {
|
||||
id: "U00003",
|
||||
name: 'Client Three',
|
||||
role: 'CLIENT',
|
||||
coach_id: coach2.id,
|
||||
},
|
||||
});
|
||||
|
||||
console.log({ coach1, coach2, client1, client2, client3 });
|
||||
}
|
||||
|
||||
main()
|
||||
.then(async () => {
|
||||
await prisma.$disconnect();
|
||||
})
|
||||
.catch(async (e) => {
|
||||
console.error(e);
|
||||
await prisma.$disconnect();
|
||||
process.exit(1);
|
||||
});
|
||||
8
public/favicon.svg
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<svg viewBox="0 0 24 24" width="48" height="48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.4 7H4.6C4.26863 7 4 7.26863 4 7.6V16.4C4 16.7314 4.26863 17 4.6 17H7.4C7.73137 17 8 16.7314 8 16.4V7.6C8 7.26863 7.73137 7 7.4 7Z" stroke="#1E40AF" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M19.4 7H16.6C16.2686 7 16 7.26863 16 7.6V16.4C16 16.7314 16.2686 17 16.6 17H19.4C19.7314 17 20 16.7314 20 16.4V7.6C20 7.26863 19.7314 7 19.4 7Z" stroke="#1E40AF" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M1 14.4V9.6C1 9.26863 1.26863 9 1.6 9H3.4C3.73137 9 4 9.26863 4 9.6V14.4C4 14.7314 3.73137 15 3.4 15H1.6C1.26863 15 1 14.7314 1 14.4Z" stroke="#1E40AF" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M23 14.4V9.6C23 9.26863 22.7314 9 22.4 9H20.6C20.2686 9 20 9.26863 20 9.6V14.4C20 14.7314 20.2686 15 20.6 15H22.4C22.7314 15 23 14.7314 23 14.4Z" stroke="#1E40AF" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M8 12H16" stroke="#1E40AF" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
1
public/file.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 391 B |
1
public/globe.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
1
public/models/xgb_activity_model.json
Normal file
1
public/next.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
1
public/vercel.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 128 B |
1
public/window.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
|
||||
|
After Width: | Height: | Size: 385 B |
20
scripts/check-links.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
|
||||
import 'dotenv/config';
|
||||
import { PrismaClient } from '../app/generated/client/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
const users = await prisma.users.findMany();
|
||||
console.log("--- All Users ---");
|
||||
users.forEach(u => console.log(`${u.name} (${u.role}): ID=${u.id}, CoachID=${u.coach_id}`));
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
39
scripts/check_logs.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
// @ts-nocheck
|
||||
import { PrismaClient } from '../app/generated/client/client';
|
||||
import * as dotenv from 'dotenv';
|
||||
dotenv.config();
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
console.log("Checking Activity Logs...");
|
||||
const logs = await prisma.activity_logs.findMany({
|
||||
take: 10,
|
||||
orderBy: {
|
||||
timestamp: 'desc',
|
||||
},
|
||||
include: {
|
||||
user: {
|
||||
select: { name: true }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (logs.length === 0) {
|
||||
console.log("No logs found.");
|
||||
} else {
|
||||
console.log(`Found ${logs.length} logs:`);
|
||||
logs.forEach(log => {
|
||||
console.log(`[${log.timestamp?.toISOString()}] User: ${log.user?.name || log.user_id} | Status: ${log.status} | Details: ${JSON.stringify(log.details)}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
33
scripts/seed_log.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
// @ts-nocheck
|
||||
import { PrismaClient } from '../app/generated/client/client';
|
||||
import * as dotenv from 'dotenv';
|
||||
dotenv.config();
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
console.log("Seeding Mock Log...");
|
||||
|
||||
// Get a user
|
||||
const user = await prisma.users.findFirst({ where: { role: 'CLIENT' } });
|
||||
if (!user) {
|
||||
console.error("No client user found to attach log to.");
|
||||
return;
|
||||
}
|
||||
|
||||
const log = await prisma.activity_logs.create({
|
||||
data: {
|
||||
user_id: user.id,
|
||||
timestamp: new Date(),
|
||||
status: 'TEST_LOG',
|
||||
confidence: '1.0',
|
||||
details: { message: "Manual verification log" }
|
||||
}
|
||||
});
|
||||
|
||||
console.log("Created Log ID:", log.id);
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => { console.error(e); process.exit(1); })
|
||||
.finally(async () => { await prisma.$disconnect(); });
|
||||