Build a multi-tenant MTB (mountain bike) course booking platform called BikeKempy.cz.
## Core Concept
A marketplace where certified MTB instructors (lectors) publish courses, manage bookings,
and receive payments. Students browse, book and pay for courses online.
Platform takes a 10% application fee via Stripe Connect.
## Pages & Routing
Public pages:
- / (home) — hero section, featured courses (6), lectors grid, CTA
- /kurzy — course listing with filters (type, region, difficulty, full-text search)
- /kurzy/:slug — course detail (gallery, sessions, lector bio, book button)
- /lektori — instructor directory with ratings
- /lektor/:id — instructor profile page
- /prihlaseni — login (email/password + Google Sign-In)
- /registrace — multi-step lector registration (5 steps + Stripe Connect)
Authenticated (lector dashboard at /dashboard):
- Přehled — earnings chart, booking stats, quick actions
- Moje kurzy — list of own courses, add/edit course, submit for approval
- Termíny — course sessions management (add date/time/capacity)
- Rezervace — booking table (customer name, email, pax, price, status)
- Zprávy — message inbox from students, reply by email
- Výdělky — monthly/yearly earnings breakdown with commission deduction
- Nastavení — Stripe Connect status, notification preferences, subscription plan
Admin panel at /admin:
- Lektoři — pending lector approvals, active/rejected list
- Kurzy — all courses, approve/reject pending, toggle active
- Platby — monthly/yearly payment overview
- Statistiky — platform-wide KPIs (lectors, bookings, revenue, avg rating)
- Gateway — Stripe/ComGate API key management, application fee %
- Faktury — invoice template configuration (logo, colors, prefix, auto-send)
## Data Model
users: id, email, password_hash, role(student/instructor/admin), phone, is_active
instructor_profiles: id, user_id, display_name, title, bio, avatar_url,
city, region, certifications[], disciplines[], plan(starter/pro/agency),
approval_status(pending/approved/rejected), stripe_account_id,
rating_avg, rating_count, created_at
courses: id, instructor_id, title, slug, short_desc, long_desc, course_type,
difficulty(zacatecnik/pokrocily/expert), region, location_text, price, currency(CZK),
max_pax, cover_image_url, gallery[], focus_tags[], includes[], requires[],
is_active, is_published, rating_avg, rating_count, created_at
course_sessions: id, course_id, session_date, time_start, time_end,
capacity, booked_count, location_text, status(open/full/cancelled)
bookings: id, session_id, course_id, customer_name, customer_email,
customer_phone, customer_address, pax, unit_price, total_price,
status(pending/paid/cancelled), created_at
gateway_config: stripe_publishable, stripe_secret(encrypted), stripe_webhook(encrypted),
stripe_test_mode, application_fee_percent(10), google_client_id,
active_gateway(stripe/comgate), comgate config fields
## Key Flows
### Lector Registration (5 steps)
1. Basic info (name, email, password, strength indicator)
2. Profile (bio 500 chars, city, phone, avatar upload)
3. Specialization (disciplines checkboxes, regions tags, certifications)
4. Plan selection (Starter free, Pro 490 CZK/mo, Agency 1490 CZK/mo)
5. Stripe Connect onboarding:
- POST /api/connect/create-account → Stripe Account::create(type=express, country=CZ)
- POST /api/connect/onboarding-link → Stripe AccountLink::create(type=account_onboarding)
- Redirect to Stripe → return to ?stripe-reg-return=1
- Poll GET /api/connect/status every 4s until charges_enabled=true
- On finishReg() send stripe_account_id with POST /api/register
### Course Lifecycle
1. Lector creates course (POST /api/courses, is_published=0)
2. Lector adds sessions (POST /api/sessions)
3. Lector submits for approval
4. Admin reviews (GET /api/courses/pending) and approves (PATCH /api/courses/:id/approve)
5. Course appears publicly (is_published=1)
6. Lector can edit price/title/desc without re-approval (PATCH /api/courses/:id)
### Booking & Payment
1. Student opens course → picks session from session picker (fresh fetch /api/courses/:id)
2. Fills customer details (name, email, phone, pax count)
3. Summary shows: total, platform fee (10%), lector net amount → instructor Stripe acct ID
4. POST /api/stripe/create-checkout with:
- line_items: course title, unit_price CZK, pax quantity
- instructor_stripe_account → payment_intent_data.transfer_data.destination
- application_fee_amount = 10% of total
- success_url includes ?payment=success&cid=&sid=&pax=&price=&email=&name=
5. Stripe redirects to success_url
6. On return: fetch /api/courses/:id for real course data, POST /api/bookings (status=paid)
7. Send invoice email (POST /api/send-invoice) with invoice number INV-YYYY-{increment from 100001}
### Invoice
HTML email invoice with: lector name/title, course name, session date/time/location,
customer details, line items (pax × unit_price = total), platform info,
invoice number (INV-YYYY-{counter stored in localStorage}), configurable header color/logo.
### Email Notifications
- Booking confirmation → student (HTML email with course details)
- Invoice → student (HTML invoice)
- Reminder 2 days before session → student + lector (bulk send from dashboard)
- Reply to student messages → lector sends from dashboard
## Authentication
- Email/password with bcrypt
- Google Sign-In (GSI) with credential callback → POST /api/auth/google (verify ID token)
- Admin: hardcoded credentials (demo) + 6-digit OTP (simulated for demo)
- After login: fetch /api/login → returns profile_id, stripe_account_id, plan, status
## Stripe Connect Architecture
- Platform account holds full payment then transfers 90% to instructor
- application_fee_amount = 10% of payment_intent total
- Instructor must complete Express onboarding before receiving payouts
- Webhook events handled: checkout.session.completed, payment_intent.payment_failed, account.updated
- Secrets stored AES-256-CTR encrypted in gateway_config table
## Localization
Czech primary, English secondary. Translation keys in flat object:
{ cs: { key: "value" }, en: { key: "value" } }
t('key') helper function. Language toggle in navbar.
## UI/UX Patterns
- Orange (#C8780A) primary accent, dark ink (#111827) text
- Cards with subtle borders, orange badges for status
- Dash layout: fixed sidebar + main content area (mobile: slide-out)
- Toast notifications (top-right, auto-dismiss 3s, type: default/err)
- Step progress indicators for multi-step flows
- Session picker modal with real-time availability
- Stripe payment overlay with progress indicator