Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,5 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts

/design
Binary file modified bun.lockb
Binary file not shown.
4 changes: 0 additions & 4 deletions next.config.mjs

This file was deleted.

32 changes: 32 additions & 0 deletions next.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { NextConfig } from 'next'

const config: NextConfig = {
reactStrictMode: true,
poweredByHeader: false,
output: 'standalone',
trailingSlash: false,
skipTrailingSlashRedirect: true,
images: {
remotePatterns: [
{
protocol: 'https',
hostname: '**'
}
]
},
experimental: {
optimizePackageImports: ['tailwindcss']
},
env: {
APP_PORT: process.env['APP_PORT'],
APP_URL: process.env['APP_URL'],
COOKIE_DOMAIN: process.env['COOKIE_DOMAIN'],
API_URL: process.env['API_URL'],
STORAGE_URL: process.env['STORAGE_URL'],
TURNSTILE_SITE_KEY: process.env['TURNSTILE_SITE_KEY'],
YANDEX_METRIKA_ID: process.env['YANDEX_METRIKA_ID'],
GOOGLE_ANALYTICS_ID: process.env['GOOGLE_ANALYTICS_ID']
}
}

export default config
13 changes: 13 additions & 0 deletions orval.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { config } from 'dotenv'
import { defineConfig } from 'orval'

config({ path: '.env' })

export default defineConfig({
client: {
input: `${process.env['API_DOCS']}/json`,
output: {
schemas: './src/generated'
}
}
})
42 changes: 37 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,57 @@
"dev": "tsx -r dotenv/config ./src/server/server.ts",
"build": "next build",
"start": "next start -p 14701",
"lint": "next lint"
"lint": "next lint",
"generate": "orval --config ./orval.config.ts"
},
"dependencies": {
"@radix-ui/react-slot": "^1.1.1",
"@hookform/resolvers": "^3.10.0",
"@kinescope/react-kinescope-player": "^0.5.3",
"@next/third-parties": "^15.1.7",
"@radix-ui/react-accordion": "^1.2.3",
"@radix-ui/react-alert-dialog": "^1.1.6",
"@radix-ui/react-avatar": "^1.1.3",
"@radix-ui/react-collapsible": "^1.1.3",
"@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-dropdown-menu": "^2.1.6",
"@radix-ui/react-label": "^2.1.1",
"@radix-ui/react-progress": "^1.1.2",
"@radix-ui/react-scroll-area": "^1.2.3",
"@radix-ui/react-separator": "^1.1.2",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-tabs": "^1.1.3",
"@radix-ui/react-tooltip": "^1.1.8",
"@tanstack/react-query": "^5.66.0",
"axios": "^1.8.4",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"dotenv": "^16.4.7",
"lucide-react": "^0.474.0",
"framer-motion": "^12.4.2",
"geist": "^1.3.1",
"input-otp": "^1.4.2",
"js-cookie": "^3.0.5",
"lucide-react": "^0.483.0",
"next": "15.1.6",
"next-themes": "^0.4.4",
"react": "19.0.0",
"react-circular-progressbar": "^2.2.0",
"react-dom": "19.0.0",
"tailwind-merge": "^3.0.1",
"tailwindcss-animate": "^1.0.7"
"react-hook-form": "^7.54.2",
"react-icons": "^5.4.0",
"react-turnstile": "^1.1.4",
"recharts": "^2.15.1",
"sonner": "^1.7.4",
"tailwind-merge": "^3.0.2",
"tailwindcss-animate": "^1.0.7",
"zod": "^3.24.1"
},
"devDependencies": {
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
"@types/js-cookie": "^3.0.6",
"@types/node": "^20",
"@types/react": "19",
"@types/react-dom": "19",
"orval": "7.4.1",
"postcss": "^8",
"prettier": "^3.4.2",
"prettier-plugin-tailwindcss": "^0.6.11",
Expand Down
Binary file added public/favicon.ico
Binary file not shown.
1 change: 0 additions & 1 deletion public/next.svg

This file was deleted.

Binary file added public/touch-icons/192x192.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/touch-icons/512x512.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 0 additions & 1 deletion public/vercel.svg

This file was deleted.

44 changes: 44 additions & 0 deletions src/api/account.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import type {
AccountResponse,
ChangeEmailRequest,
ChangePasswordRequest,
CreateUserRequest,
CreateUserResponse,
PasswordResetRequest,
SendPasswordResetRequest
} from '../generated'
import { setSessionToken } from '../lib/cookies/session'

import { api, instance } from './instance'

export const getMe = async () =>
await instance
.get<AccountResponse>('/auth/account')
.then(response => response.data)

export const createAccount = async (data: CreateUserRequest) => {
const response = await api.post<CreateUserResponse>(
'/auth/account/create',
data
)

if (response.data.token) {
setSessionToken(response.data.token)

instance.defaults.headers['X-Session-Token'] = response.data.token
}

return response.data
}

export const sendPasswordReset = (data: SendPasswordResetRequest) =>
api.post('/auth/account/reset_password', data)

export const passwordReset = (data: PasswordResetRequest) =>
api.patch('/auth/account/reset_password', data)

export const changeEmail = (data: ChangeEmailRequest) =>
instance.patch('/auth/account/change/email', data)

export const changePassword = (data: ChangePasswordRequest) =>
instance.patch('/auth/account/change/password', data)
24 changes: 24 additions & 0 deletions src/api/course.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { CourseResponse, LessonResponse } from '../generated'

import { api, instance } from './instance'

export const getCourses = async () =>
await api.get<CourseResponse[]>('/courses').then(response => response.data)

export const getPopularCourses = async () =>
await api
.get<CourseResponse[]>('/courses/popular')
.then(response => response.data)

export const getCourse = async (slug: string) =>
await api
.get<CourseResponse>(`/courses/${slug}`)
.then(response => response.data)

export const getCourseLessons = async (id: string) =>
await instance
.get<LessonResponse[]>(`/courses/${id}/lessons`)
.then(response => response.data)

export const incrementCourseViews = (id: string) =>
api.patch(`/courses/${id}/views`)
8 changes: 8 additions & 0 deletions src/api/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export * from './account'
export * from './course'
export * from './instance'
export * from './lesson'
export * from './mfa'
export * from './progress'
export * from './session'
export * from './users'
15 changes: 15 additions & 0 deletions src/api/instance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import axios from 'axios'

import { APP_CONFIG } from '../constants/app'
import { getSessionToken } from '../lib/cookies/session'

export const api = axios.create({
baseURL: APP_CONFIG.apiUrl
})

export const instance = axios.create({
baseURL: APP_CONFIG.apiUrl,
headers: {
'X-Session-Token': getSessionToken() ?? ''
}
})
16 changes: 16 additions & 0 deletions src/api/lesson.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { CreateLessonRequest, LessonResponse } from '../generated'

import { api, instance } from './instance'

export const getLesson = async (slug: string) =>
await api
.get<LessonResponse>(`/lessons/${slug}`)
.then(response => response.data)

export const getCompletedLessons = async (courseId: string) =>
await instance
.get<string[]>(`/lessons/${courseId}/progress`)
.then(response => response.data)

export const createLesson = (data: CreateLessonRequest) =>
instance.post('/lessons', data)
37 changes: 37 additions & 0 deletions src/api/mfa.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type {
LoginSessionResponse,
MfaStatusResponse,
MfaVerifyRequest,
TotpDisableRequest,
TotpEnableRequest,
TotpGenerateSecretResponse
} from '../generated'

import { api, instance } from './instance'

export const fetchMfaStatus = async () =>
await instance
.get<MfaStatusResponse>('/auth/mfa')
.then(response => response.data)

export const fetchRecovery = async () =>
await instance
.get<string[]>('/auth/mfa/recovery')
.then(response => response.data)

export const regenerateRecovery = () =>
instance.patch<string>('/auth/mfa/recovery')

export const totpEnable = (data: TotpEnableRequest) =>
instance.put('/auth/mfa/totp', data)

export const totpGenerateSecret = async () =>
await instance
.post<TotpGenerateSecretResponse>('/auth/mfa/totp')
.then(response => response.data)

export const totpDisable = (data: TotpDisableRequest) =>
instance.delete('/auth/mfa/totp', { data })

export const verifyMfa = (data: MfaVerifyRequest) =>
api.post<LoginSessionResponse>('/auth/mfa/verify', data)
16 changes: 16 additions & 0 deletions src/api/progress.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type {
CreateProgressRequest,
CreateProgressResponse
} from '../generated'

import { instance } from './instance'

export const createProgress = async (data: CreateProgressRequest) =>
await instance
.put<CreateProgressResponse>('/progress', data)
.then(response => response.data)

export const getCourseProgress = async (courseId: string) =>
await instance
.get<number>(`/progress/${courseId}`)
.then(response => response.data)
31 changes: 31 additions & 0 deletions src/api/session.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type {
LoginMfaResponse,
LoginRequest,
LoginSessionResponse,
SessionResponse
} from '../generated'
import { removeSessionToken } from '../lib/cookies/session'

import { api, instance } from './instance'

export const login = async (data: LoginRequest) =>
await api
.post<
LoginSessionResponse | LoginMfaResponse
>('/auth/session/login', data)
.then(response => response.data)

export const logout = async () =>
await instance
.post<boolean>('/auth/session/logout')
.then(() => removeSessionToken())

export const getSessions = async () =>
await instance
.get<SessionResponse[]>('/auth/session/all')
.then(response => response.data)

export const revokeSession = (id: string) =>
instance.delete(`/auth/session/${id}`)

export const removeAllSessions = () => instance.delete('/auth/session/all')
33 changes: 33 additions & 0 deletions src/api/users.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type {
LeaderResponse,
MeProgressResponse,
MeStatisticsResponse,
PatchUserRequest
} from '../generated'

import { api, instance } from './instance'

export const getLeaders = async () =>
await api
.get<LeaderResponse[]>('/users/leaders')
.then(response => response.data)

export const getMeStatistics = async () =>
await instance
.get<MeStatisticsResponse>('/users/@me/statistics')
.then(response => response.data)

export const getMeProgress = async () =>
await instance
.get<MeProgressResponse[]>('/users/@me/progress')
.then(response => response.data)

export const changeAvatar = (formData: FormData) =>
instance.patch('/users/@me/avatar', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})

export const patchUser = (data: PatchUserRequest) =>
instance.patch('/users/@me', data)
Loading