Skip to content
farhan-shafiPublic

About

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

πŸ‹οΈ Reppod

A two-sided coaching SaaS for personal trainers and their clients.

Trainers build workout programs, assign them to clients, attach demo videos, and track engagement. Clients get their own portal to watch demos, log their own sessions, see their progress, and message their coach. Built as a full-stack MERN-style app on Next.js 16 + MongoDB, with motion-rich UI throughout.

This is a portfolio project demonstrating real-product patterns: role-based two-sided auth, an invite flow, drag-and-drop builders, file/video uploads, engagement analytics, and in-app notifications.

View the live demo β†’

Reppod product overview


✨ Features

Marketing site

  • Animated landing page β€” parallax hero, 3D-tilt feature cards, scroll-triggered count-up stats, animated pricing toggle, buttery smooth scrolling (Lenis).

Authentication & roles

  • Email/password auth (Auth.js v5 / NextAuth) with JWT sessions, bcrypt hashing.
  • Optional Google OAuth (auto-enabled when credentials are provided).
  • Two roles β€” trainer and client β€” with role-based routing: /dashboard for trainers, /app for clients. The wrong role is redirected.
  • Invite flow β€” a trainer adds a client, copies an invite link, and the client sets a password to create their own linked account.

Trainer dashboard (/dashboard)

  • Clients β€” full CRUD with an animated list, search, status (active/paused/ archived), goals, and per-client invite links.
  • Workout builder β€” drag-and-drop exercise ordering (dnd-kit) with per-exercise sets / reps / rest, plus an exercise picker filtered by muscle group.
  • Demo videos β€” attach a Cloudinary-hosted video to any exercise.
  • Assignments β€” assign workouts from the library to clients.
  • Progress β€” log sessions on a client's behalf; view an animated volume chart (Recharts) + session history.
  • Engagement β€” per-client video watch-% and exercise completion at a glance.
  • Messaging β€” a two-way chat thread with each client.
  • Notifications β€” an in-app bell with unread counts.
  • Settings β€” profile, business name, units (kg/lb), avatar upload, password.

Client portal (/app)

  • Today β€” assigned workouts + quick stats.
  • Workout view β€” watch each exercise's demo video (watch progress tracked automatically) and log your own sets/reps/weight.
  • Progress β€” personal volume chart + full session history.
  • Messages β€” chat with your coach.
  • Settings β€” profile, units, avatar, password.
  • Mobile-friendly bottom navigation.

🧱 Tech stack

Layer Choice
Framework Next.js 16 (App Router, Turbopack, React 19)
Language TypeScript
Database MongoDB + Mongoose
Auth Auth.js v5 (NextAuth) β€” Credentials + optional Google
Validation Zod (shared client/server schemas)
Styling Tailwind CSS v4
Animation Framer Motion (components) Β· Lenis (smooth scroll)
Drag & drop dnd-kit
Charts Recharts
Media Cloudinary (unsigned uploads β€” avatars & exercise videos)
Icons lucide-react

πŸ›οΈ Architecture

Two-sided, role-based

Both roles share one sign-in page and one User collection. The role field (trainer | client) drives everything:

Role Home Capabilities
Trainer /dashboard Build workouts, manage clients, assign, log on behalf, review, chat
Client /app Watch demos, log own sessions, view progress, chat with coach

Routing is enforced in two places:

  • src/proxy.ts (Next.js 16's renamed middleware) + src/auth.config.ts β€” an edge-safe authorized callback gates /dashboard (trainer-only) and /app (client-only), and redirects logged-in users away from the auth pages to their role's home.
  • Server components additionally call requireTrainer() / requireClient() from src/lib/auth-helpers.ts as a defense-in-depth check.

Auth.js config is split so middleware stays edge-safe: auth.config.ts holds only edge-compatible logic (callbacks, routing), while auth.ts adds the Credentials/Google providers and Mongoose access used in the Node runtime.

The invite flow

Trainer adds client ─▢ server generates inviteToken ─▢ trainer copies /invite/<token>
        β”‚                                                        β”‚
        β–Ό                                                        β–Ό
Client record (inviteStatus: pending)                 Client opens link, sets password
        β”‚                                                        β”‚
        └──────────────── linked via Client.user ◀── User created (role: client)
                                                       inviteStatus: accepted

Tokens are generated on demand (POST /api/clients/[id]/invite), which also backfills clients created before the field existed.

Data flow

  • Server Components read directly from MongoDB via Mongoose and pass plain serialized objects to client components.
  • Client Components mutate through Route Handlers (/api/**), which validate input with shared Zod schemas and enforce ownership.
  • Notifications are written as a best-effort side effect of key actions (assign, log, message, invite-accepted) and polled by the bell component.

Ownership & security

  • Every /api route checks the session and that the resource belongs to the caller (a trainer only touches their own clients/workouts; a client only their own data, resolved via Client.user).
  • Passwords hashed with bcrypt (cost 12); passwordHash is select: false.
  • Sign-in, registration, and invite acceptance use a MongoDB-backed rate limiter shared across serverless instances. Limiter keys are SHA-256 hashes, not raw email or IP values.
  • New passwords require 10+ characters with uppercase, lowercase, and a number.
  • Google OAuth identities are resolved to the same MongoDB user IDs used by all protected routes.
  • Deletes cascade: removing a client also removes their assignments, sessions, messages, notifications, and linked login β€” nothing is left orphaned.

πŸ—‚οΈ Data models

Model Purpose
User Trainer or client account (role, bio, units, businessName, image)
Client A trainer's client record; optionally linked to a User via invite
Workout A program; embeds blocks (exercise + sets/reps/rest + videoUrl)
WorkoutAssignment Junction: which workout is assigned to which client
WorkoutSession A logged session (per-set reps Γ— weight); powers progress + volume
Message One chat message in a trainer↔client thread
Notification An in-app notification for a user
VideoProgress Per (client, workout, exercise) watch progress: %, completed
RateLimit Expiring counters for public authentication endpoints

Exercises themselves are a static catalog in src/lib/exercises.ts (muscle group

  • equipment), keeping the demo self-contained.

πŸ“ Project structure

src/
β”œβ”€β”€ app/
β”‚   β”œβ”€β”€ (auth)/                 # Sign-in / sign-up (shared animated layout)
β”‚   β”œβ”€β”€ api/                    # Route Handlers
β”‚   β”‚   β”œβ”€β”€ auth/[...nextauth]/  #   Auth.js handlers
β”‚   β”‚   β”œβ”€β”€ register/           #   Trainer sign-up
β”‚   β”‚   β”œβ”€β”€ invite/[token]/     #   Validate + accept invite
β”‚   β”‚   β”œβ”€β”€ clients/[id]/...     #   Clients + nested assignments/sessions/messages/invite
β”‚   β”‚   β”œβ”€β”€ workouts/[id]/       #   Workout CRUD
β”‚   β”‚   β”œβ”€β”€ me/...               #   Client-facing: workouts, sessions, messages, video-progress, profile, password
β”‚   β”‚   └── notifications/       #   List + mark-read
β”‚   β”œβ”€β”€ dashboard/              # Trainer area (sidebar + topbar + mobile nav)
β”‚   β”‚   β”œβ”€β”€ clients/[id]/        #   Tabbed detail: Overview/Workouts/Progress/Engagement/Messages
β”‚   β”‚   β”œβ”€β”€ workouts/[new|[id]]  #   Builder + detail
β”‚   β”‚   └── settings/
β”‚   β”œβ”€β”€ app/                    # Client portal (own nav)
β”‚   β”‚   β”œβ”€β”€ workouts/[id]/       #   Watch videos + log session
β”‚   β”‚   β”œβ”€β”€ progress/  messages/  settings/
β”‚   β”œβ”€β”€ invite/[token]/         # Public invite-accept page
β”‚   β”œβ”€β”€ layout.tsx              # Root: fonts, SessionProvider, Lenis
β”‚   └── page.tsx                # Landing page
β”œβ”€β”€ components/
β”‚   β”œβ”€β”€ sections/               # Landing page sections
β”‚   β”œβ”€β”€ dashboard/  client-portal/  client-detail/  workouts/  messaging/  notifications/  settings/
β”‚   β”œβ”€β”€ SmoothScrollProvider.tsx  AuthSessionProvider.tsx  Navbar.tsx
β”œβ”€β”€ lib/
β”‚   β”œβ”€β”€ mongoose.ts  mongodb.ts # Cached connections
β”‚   β”œβ”€β”€ auth-helpers.ts  api-helpers.ts
β”‚   β”œβ”€β”€ exercises.ts  units.ts  utils.ts
β”‚   β”œβ”€β”€ security.ts              # Password/email policy + hashed limiter keys
β”‚   β”œβ”€β”€ rate-limit.ts            # MongoDB-backed fixed-window rate limiting
β”‚   └── schemas/                # Zod: client, workout, progress, video, profile
β”œβ”€β”€ models/                     # Mongoose models (see table above)
β”œβ”€β”€ auth.ts  auth.config.ts     # Auth.js (split for edge safety)
└── proxy.ts                    # Next 16 middleware β†’ role-based routing

πŸš€ Getting started

Prerequisites

  • Node.js 20.9+
  • A MongoDB database (local or MongoDB Atlas β€” free tier is plenty)

Setup

git clone https://github.com/farhan-shafi/reppod.git
cd reppod
npm install
cp .env.example .env.local   # then fill in the values below
npm run dev                  # http://localhost:3000

Environment variables

Variable Required Notes
AUTH_SECRET βœ… openssl rand -base64 32
MONGODB_URI βœ… Atlas string or mongodb://localhost:27017/reppod
GOOGLE_CLIENT_ID / ..._SECRET β€” Enables Google sign-in when both are set
NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME β€” Enables avatar + exercise-video uploads
NEXT_PUBLIC_CLOUDINARY_UPLOAD_PRESET β€” Unsigned preset restricted as described below

NEXT_PUBLIC_* vars are read at server start β€” restart npm run dev after changing them.

For Cloudinary, the upload preset is the security boundary because browser-side checks can be bypassed. Restrict the preset in Cloudinary to the reppod folder, JPG/PNG/WebP/AVIF images and MP4/WebM/MOV videos, with maximum sizes of 10 MB for images and 50 MB for videos. Do not reuse that unsigned preset for other apps.

Scripts

npm run dev      # start dev server (Turbopack)
npm test         # security policy unit tests
npm run lint     # ESLint
npm run build    # production build
npm run start    # serve the production build

πŸ§ͺ Try the full loop

  1. Sign up at /sign-up β†’ you're a trainer in /dashboard.
  2. Build a workout (Workouts β†’ New) β€” drag in exercises, set reps, optionally attach a demo video.
  3. Add a client β†’ copy their invite link.
  4. Open the invite link in an incognito window β†’ set a password β†’ you're now in the client portal (/app).
  5. As the client: open the workout, watch the video, log your sets.
  6. Back as the trainer: open the client β†’ Engagement shows watch %, Progress shows the logged session, and Messages works both ways.

πŸ—ΊοΈ Roadmap

  • Animated marketing site
  • Two-sided auth + invite flow + role routing
  • Clients CRUD, workout builder, assignments
  • Progress logging + charts
  • Two-way messaging + notifications
  • Settings, profile, Cloudinary avatars
  • Per-exercise demo videos + watch tracking + engagement analytics
  • Billing (LemonSqueezy β€” Pakistan/global-friendly merchant of record)
  • Password reset via email
  • Streak / achievement animations

πŸ“ Notes

  • Built on Next.js 16 β€” Turbopack is the default; middleware is proxy.ts.
  • All interactive UI is client components; everything else is server-rendered for speed and SEO.
  • Demo data is self-contained β€” no external services are required to run the core app (Cloudinary and Google are optional enhancements).

License

MIT Β© 2026 Farhan Shafi

About

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages