React + Vite + TypeScript + Tailwind CSS.
src/
├── components/
│ ├── feature/ # Business components
│ │ ├── CourseCard.tsx
│ │ ├── Quiz.tsx
│ │ ├── VideoPlayer.tsx
│ │ ├── CloudflareStreamPlayer.tsx
│ │ └── ...
│ ├── layout/ # Layout components
│ │ ├── AppRoutes.tsx
│ │ ├── BottomNav.tsx
│ │ ├── MobileLayout.tsx
│ │ └── DashLayout.tsx
│ └── ui/ # Base UI components (shadcn)
│ ├── button.tsx
│ ├── drawer.tsx
│ └── ...
├── hooks/
│ ├── useApi.ts # Data fetching hooks
│ ├── useSupport.ts # Support chat hooks
│ └── useCloudflareStream.tsx
├── lib/
│ ├── api.ts # API client
│ ├── demo.ts # Demo mode utilities
│ ├── telegram.ts # Telegram WebApp utils
│ ├── markdown.ts # Markdown parser (with DOMPurify)
│ ├── i18n.tsx # Internationalization
│ └── utils.ts # Utility functions
├── locales/
│ ├── en.json
│ ├── ru.json
│ └── uk.json
├── pages/
│ ├── HomePage.tsx
│ ├── CoursePage.tsx
│ ├── LessonPage.tsx
│ ├── MyCoursesPage.tsx
│ ├── ProfilePage.tsx
│ ├── SupportPage.tsx
│ ├── DashHomePage.tsx
│ ├── DashUsersPage.tsx
│ ├── DashTransactionsPage.tsx
│ ├── DashReviewsPage.tsx
│ ├── DashCoursesPage.tsx
│ └── DashSupportPage.tsx
├── assets/
├── App.tsx
├── index.css
└── main.tsx
/ HomePage
/course/:id CoursePage
/course/:id/learn LessonPage (fullscreen, no bottom nav)
/my-courses MyCoursesPage
/profile ProfilePage
/support SupportPage
/dash DashHomePage (admin)
/dash/users DashUsersPage
/dash/transactions DashTransactionsPage
/dash/reviews DashReviewsPage
/dash/courses DashCoursesPage
/dash/support DashSupportPage
import * as api from '@/lib/api'
// Get featured courses
const courses = await api.getFeaturedCourses()
// Get course details
const course = await api.getCourse(id)
// Get lessons (requires purchase)
const lessons = await api.getCourseLessons(courseId)
// Mark lesson complete
await api.markLessonComplete(courseId, lessonId)
// Get user profile
const user = await api.getProfile()
// Submit review
await api.submitReview(courseId, rating, comment)import {
useCourses,
useCourse,
useCourseLessons,
useCourseProgress,
useProfile
} from '@/hooks/useApi'
function Component() {
const { data, loading, error, refetch } = useCourses()
// ...
}import { useI18n } from '@/lib/i18n'
function Component() {
const { t, locale, setLocale } = useI18n()
return <p>{t('home.welcome')}</p>
}Supported locales: en, ru, uk
Language detected from:
- Telegram WebApp language
- Browser language
- Falls back to
en
import {
getTelegramUser,
getTelegramInitData,
hapticFeedback,
showPopup,
openLink
} from '@/lib/telegram'
// Get user info
const user = getTelegramUser()
// Get initData for API calls
const initData = getTelegramInitData()
// Haptic feedback
hapticFeedback('impact', 'medium')
// Show popup
showPopup({ message: 'Hello!' })
// Open link
openLink('https://example.com')For development without Telegram WebApp.
import { isDemoMode, getDemoProfile, setDemoRole } from '@/lib/demo'
if (isDemoMode()) {
const profile = getDemoProfile()
setDemoRole('admin') // or 'user'
}Demo data stored in localStorage with cg_demo_ prefix.
npm run dev # Development server (localhost:5173)
npm run demo # Development with VITE_DEMO_MODE=true
npm run build # Production build
npm run preview # Preview production build
npm run lint # ESLintOutput: dist/
.env for development:
VITE_API_URL=http://localhost:3001/api
VITE_DEMO_MODE=true
Production: API calls use relative URLs (/api).
- Tailwind CSS for utility classes
- shadcn/ui for base components
- Framer Motion for animations
- Custom CSS in
index.css
Theme colors defined in tailwind.config.js:
accentpurple- Primary accent- Dark theme by default
HLS video player for Cloudflare Stream videos that requests a signed URL from /api/stream/sign-url. It must receive the current courseId so the server can confirm the user purchased the course before granting the token.
Interactive quiz with single/multiple choice questions.
Payment flow (PayPal, Telegram Stars).
Course review submission.
Animated wave gradient hero section.