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
1 change: 0 additions & 1 deletion src/Header/Nav/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,6 @@ export const HeaderNav: React.FC<{ data: HeaderType; locale: SiteLocale }> = ({
return (
<div ref={rootRef} className="contents">
<nav className="flex items-center gap-0.5" aria-label={t.projectName}>
{/* mobile: a single hamburger holds nav, search, account, and language */}
<button
type="button"
onClick={toggleMobile}
Expand Down
2 changes: 0 additions & 2 deletions src/HomeCalendar/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ import type { GlobalConfig } from 'payload'
import { getHomeContent } from '@/components/Home/content'
import { revalidateHomeCalendar } from './hooks/revalidateHomeCalendar'

// Admin form defaults mirror the hardcoded landing content (content.ts), so the
// first save starts from what the site already shows.
const defaults = (locale: unknown) => getHomeContent(locale === 'en' ? 'en' : 'uk').calendar

export const HomeCalendar: GlobalConfig = {
Expand Down
2 changes: 0 additions & 2 deletions src/actions/commentsAndLikes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import configPromise from '@payload-config'
import { revalidateTag } from 'next/cache'
import { getSession } from '@/lib/auth/getSession'

/** True when a Local API write was rejected by a rate-limit hook (429). */
const isRateLimited = (err: unknown): boolean => err instanceof APIError && err.status === 429

type TargetCollection = 'posts' | 'courses'
Expand Down Expand Up @@ -249,7 +248,6 @@ export type LikeInfo = {
count: number
}

/** Invalidate the cached grouped counts (src/utilities/contentCounts.ts) after a write. */
function revalidateCounts(kind: 'likes' | 'comments', targetCollection: LikeTargetCollection) {
if (targetCollection === 'posts' || targetCollection === 'courses') {
revalidateTag(`${kind}-counts-${targetCollection}`)
Expand Down
2 changes: 0 additions & 2 deletions src/actions/xp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,9 @@ export type MyXp = {
level: number
intoLevel: number
span: number
/** Avatar URL from the users doc — the better-auth session payload omits `image`. */
image: string | null
}

/** Total XP and level for the signed-in user (30 XP/step, 100 XP/passed quiz). */
export async function getMyXp(): Promise<MyXp | null> {
const session = await getSession().catch(() => null)
if (!session?.user) return null
Expand Down
1 change: 0 additions & 1 deletion src/app/(frontend)/[locale]/courses/[slug]/loading.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// Instant skeleton shown while a course overview renders — mirrors its layout.
export default function Loading() {
return (
<div className="animate-pulse pb-16">
Expand Down
3 changes: 0 additions & 3 deletions src/app/(frontend)/[locale]/courses/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,6 @@ import { RewardPill } from '@/components/brand'
import { courseXp } from '@/utilities/xp'
import { plural } from '@/utilities/plural'

// Per-user state (enrollment, progress) is fetched client-side via
// CourseUserStateProvider, so the page itself is the same for everyone and
// can be served from the ISR cache. Enrollment mutations revalidate it by path.
export const revalidate = 300

export async function generateStaticParams() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,6 @@ export default async function StepViewerPage({ params: paramsPromise }: Args) {
return (
<div className="pt-10 pb-16">
<div className="container max-w-7xl">
{/* Top bar */}
<div className="flex items-center justify-between gap-4 mb-4 flex-wrap">
<Link
href={`${prefix}/courses/${slug}`}
Expand All @@ -124,7 +123,6 @@ export default async function StepViewerPage({ params: paramsPromise }: Args) {
step body and the steps sidebar both start on row 2 — their tops line up.
*/}
<div className="grid grid-cols-1 gap-x-8 lg:grid-cols-[minmax(0,1fr)_18rem] xl:grid-cols-[minmax(0,1fr)_20rem]">
{/* Title + meta — spans only the content column */}
<div className="min-w-0 lg:col-start-1 lg:row-start-1">
<h1 className="heading-display mb-2.5 text-[clamp(24px,3vw,34px)]">{stepTitle}</h1>

Expand All @@ -142,7 +140,6 @@ export default async function StepViewerPage({ params: paramsPromise }: Args) {
</div>
</div>

{/* Main content */}
<div className="min-w-0 lg:col-start-1 lg:row-start-2">
<div className="mb-8">
{step.blockType === 'richTextStep' && step.content && (
Expand Down Expand Up @@ -178,7 +175,6 @@ export default async function StepViewerPage({ params: paramsPromise }: Args) {
)}
</div>

{/* Navigation footer */}
{isLastStep && quizEnabled && !quizReady && (
<p className="mb-3 flex items-center justify-end gap-1.5 text-[11.5px] font-semibold uppercase tracking-[0.08em] text-steel">
<Lock className="h-3.5 w-3.5 flex-none" />
Expand Down Expand Up @@ -218,7 +214,6 @@ export default async function StepViewerPage({ params: paramsPromise }: Args) {
</div>
</div>

{/* Sidebar steps list — visible on desktop */}
<aside className="mt-8 w-full min-w-0 lg:col-start-2 lg:row-start-2 lg:mt-0">
<div className="rounded-2xl border border-line bg-card p-4 lg:sticky lg:top-24">
<h3 className="mb-3.5 px-1 font-display text-xs font-semibold uppercase tracking-[0.2em] text-fog">
Expand Down
14 changes: 2 additions & 12 deletions src/app/(frontend)/[locale]/courses/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,8 @@ import { getStepIds, isCourseComplete } from '@/utilities/courseCompletion'
import type { Course, Enrollment, QuizAttempt } from '@/payload-types'
import type { Payload } from 'payload'

/**
* Timestamped XP log for period leaderboards; total XP stays derived from
* enrollments, so a failed log write must not fail the mutation that earned it.
*/
// Total XP stays derived from enrollments, so a failed log write must not fail the
// mutation that earned it.
async function logXpEvent(
payload: Payload,
data: { user: number; course: number; kind: 'step' | 'quiz'; amount: number },
Expand All @@ -26,14 +24,12 @@ async function logXpEvent(
}
}

/** Bust cached course pages (both locales) after an enrollment mutation. */
function revalidateCoursePages(slug: string) {
for (const prefix of ['', '/en']) {
revalidatePath(`${prefix}/courses/${slug}`)
revalidatePath(`${prefix}/courses/${slug}/steps/[stepIndex]`, 'page')
revalidatePath(`${prefix}/courses/${slug}/quiz`)
}
// Cached aggregate counters (src/utilities/contentCounts.ts)
revalidateTag('course-enrollment-stats')
}

Expand All @@ -42,10 +38,6 @@ export type MyCourseStatuses = {
inProgress: number[]
}

/**
* Course ids the signed-in user has completed / started. Fetched client-side
* so the catalog page itself can stay statically cached for everyone.
*/
export async function getMyCourseStatuses(): Promise<MyCourseStatuses> {
const session = await getSession().catch(() => null)
if (!session?.user) return { completed: [], inProgress: [] }
Expand Down Expand Up @@ -225,8 +217,6 @@ export async function getEnrollment(
],
},
limit: 1,
// Consumed client-side (CourseUserState) — ids are enough, don't populate
// the full user/course docs into the response.
depth: 0,
})

Expand Down
4 changes: 0 additions & 4 deletions src/app/(frontend)/[locale]/courses/category/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ import { getCatalogData } from '@/lib/courses/getCatalogData'
import { plural } from '@/utilities/plural'
import type { Media as MediaType } from '@/payload-types'

// Session-free on the server (viewer progress loads client-side in CourseGrid),
// so category pages serve from the ISR cache like the main catalog.
export const revalidate = 300

export async function generateStaticParams() {
Expand Down Expand Up @@ -77,8 +75,6 @@ export default async function CourseCategoryPage({ params: paramsPromise }: Args
style={{ backgroundImage: `url(${imageUrl})` }}
/>
)}
{/* Lighter at the top than the course hero so the category cover
actually reads through; still settles into solid void for the grid. */}
<div
className="absolute inset-0"
style={{
Expand Down
2 changes: 0 additions & 2 deletions src/app/(frontend)/[locale]/courses/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ import { CategoryCarousels } from '@/components/Courses/CategoryCarousels'
import { PageHead } from '@/components/brand'
import { getCatalogData } from '@/lib/courses/getCatalogData'

// The catalog is the same for everyone — per-user progress badges are fetched
// client-side (useMyCourseStatuses), so this page can be served from the ISR cache.
export const revalidate = 300

export function generateStaticParams() {
Expand Down
2 changes: 0 additions & 2 deletions src/app/(frontend)/[locale]/leaderboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ import {
getCachedPeriodLeaderboard,
} from '@/utilities/leaderboard'

// The leaderboard is the same for everyone, so it can be served from the ISR
// cache; XP-awarding mutations revalidate the `xp-leaderboard` tag.
export const revalidate = 300

export function generateStaticParams() {
Expand Down
1 change: 0 additions & 1 deletion src/app/(frontend)/[locale]/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ export default async function LoginPage({ params, searchParams }: Args) {
const { locale } = await params
const { redirect: redirectParam, reset } = await searchParams
const home = locale === defaultLocale ? '/' : `/${locale}`
// sanitized return path; undefined keeps each form's own default
const redirectTo = redirectParam ? safeRedirectPath(redirectParam, home) : undefined

const session = await getSession()
Expand Down
5 changes: 2 additions & 3 deletions src/app/(frontend)/[locale]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,8 @@ import { NewsSectionServer, NewsSectionSkeleton } from '@/components/Home/NewsSe
import { CoursesGridServer, CoursesGridSkeleton } from '@/components/Home/CoursesServer'
import { CategoryCarousels } from '@/components/Courses/CategoryCarousels'

// Home has no per-request personalization on the server — safe to serve from ISR cache.
// generateStaticParams is required for that: without it, a dynamic-segment route
// is rendered per request and `revalidate` has no effect.
// generateStaticParams is required for `revalidate` to take effect: without it a
// dynamic-segment route is rendered per request.
export const revalidate = 300

export function generateStaticParams() {
Expand Down
1 change: 0 additions & 1 deletion src/app/(frontend)/[locale]/posts/[slug]/loading.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// Instant skeleton shown while an article renders — mirrors the PostHero layout.
export default function Loading() {
return (
<article className="animate-pulse pb-16">
Expand Down
2 changes: 0 additions & 2 deletions src/app/(frontend)/[locale]/posts/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@ import { ShareButtons } from '@/components/ShareButtons'
import { getServerSideURL } from '@/utilities/getURL'
import { getFrontendMessages } from '@/utilities/i18n'

// Refresh periodically so the baked-in like/comment counts don't go stale;
// content edits still revalidate immediately via the collection hook.
export const revalidate = 600

export async function generateStaticParams() {
Expand Down
7 changes: 0 additions & 7 deletions src/app/(frontend)/[locale]/profile/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,6 @@ export default async function ProfilePage({ params }: Args) {
}),
])

// XP scoreboard: 30 XP per completed step, 100 XP per passed final quiz.
let stepsDone = 0
let quizzesPassed = 0
for (const enrollment of enrollments.docs) {
Expand Down Expand Up @@ -113,7 +112,6 @@ export default async function ProfilePage({ params }: Args) {
return (
<div className="container max-w-2xl py-16">
<div className="mb-8 flex flex-col items-center gap-4">
{/* avatar with XP level ring */}
<div
className="h-24 w-24 rounded-full p-[4px]"
style={{
Expand Down Expand Up @@ -186,7 +184,6 @@ export default async function ProfilePage({ params }: Args) {
</div>
</div>

{/* XP progress to next level */}
<div className="rounded-2xl border border-line-2 bg-[linear-gradient(150deg,rgb(4_40_113/0.5),var(--navy))] p-6">
<div className="mb-2 flex items-center justify-between text-[11px] font-bold uppercase tracking-[0.1em] text-fog">
<span>{t.profileToNextLevel}</span>
Expand Down Expand Up @@ -219,7 +216,6 @@ export default async function ProfilePage({ params }: Args) {
</div>
</div>

{/* courses in progress */}
<h2 className={sectionHeading}>{t.profileMyCourses}</h2>
{inProgress.length === 0 && completed.length === 0 ? (
<div className="rounded-2xl border border-line bg-card p-8 text-center">
Expand Down Expand Up @@ -277,7 +273,6 @@ export default async function ProfilePage({ params }: Args) {
</div>
)}

{/* certificates */}
{completed.length > 0 && (
<>
<h2 className={sectionHeading}>{t.profileCertificates}</h2>
Expand Down Expand Up @@ -323,7 +318,6 @@ export default async function ProfilePage({ params }: Args) {
</>
)}

{/* recent quiz attempts */}
{recentAttempts.docs.length > 0 && (
<>
<h2 className={sectionHeading}>{t.profileRecentQuizzes}</h2>
Expand Down Expand Up @@ -370,7 +364,6 @@ export default async function ProfilePage({ params }: Args) {
</>
)}

{/* account info */}
<div className="mt-10 space-y-5 rounded-2xl border border-line bg-card p-6">
<div className="flex items-center gap-3">
<Mail className="h-5 w-5 shrink-0 text-orange" />
Expand Down
1 change: 0 additions & 1 deletion src/app/(frontend)/[locale]/register/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ export default async function RegisterPage({ params, searchParams }: Args) {
const { redirect: redirectParam } = await searchParams

const home = locale === defaultLocale ? '/' : `/${locale}`
// sanitized return path; undefined keeps each form's own default
const redirectTo = redirectParam ? safeRedirectPath(redirectParam, home) : undefined

const session = await getSession()
Expand Down
6 changes: 0 additions & 6 deletions src/app/(frontend)/[locale]/users/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,6 @@ export default async function PublicProfilePage({ params }: Args) {
depth: 1,
})

// Same XP math as the private profile page.
let stepsDone = 0
let quizzesPassed = 0
for (const enrollment of enrollments.docs) {
Expand All @@ -102,7 +101,6 @@ export default async function PublicProfilePage({ params }: Args) {
return (
<div className="container max-w-2xl py-16">
<div className="mb-8 flex flex-col items-center gap-4">
{/* avatar with XP level ring */}
<div
className="h-24 w-24 rounded-full p-[4px]"
style={{
Expand Down Expand Up @@ -158,7 +156,6 @@ export default async function PublicProfilePage({ params }: Args) {
)}
</div>

{/* stats */}
<div className="rounded-2xl border border-line-2 bg-[linear-gradient(150deg,rgb(4_40_113/0.5),var(--navy))] p-6">
<div className="grid grid-cols-3 gap-3 text-center">
<div>
Expand All @@ -182,7 +179,6 @@ export default async function PublicProfilePage({ params }: Args) {
</div>
</div>

{/* completed courses — no scores, no certificate downloads */}
{completed.length > 0 && (
<>
<h2 className="heading-display mb-4 mt-10 text-xl tracking-[0.06em]">
Expand Down Expand Up @@ -218,14 +214,12 @@ export default async function PublicProfilePage({ params }: Args) {
</>
)}

{/* latest comments — streamed after first paint so they never block the page */}
{user.hideProfileComments !== true && (
<Suspense fallback={<ProfileLatestCommentsSkeleton />}>
<ProfileLatestComments userId={user.id} locale={locale} />
</Suspense>
)}

{/* joined date */}
{joinedDate && (
<div className="mt-10 flex items-center gap-3 rounded-2xl border border-line bg-card p-6">
<Calendar className="h-5 w-5 shrink-0 text-orange" />
Expand Down
2 changes: 0 additions & 2 deletions src/app/(frontend)/next/preview/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,6 @@ export async function GET(req: NextRequest): Promise<Response> {
return new Response('You are not allowed to preview this page', { status: 403 })
}

// You can add additional checks here to see if the user is allowed to preview this page

draft.enable()

redirect(path)
Expand Down
4 changes: 0 additions & 4 deletions src/app/api/auth/verify-registration/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ async function handleSendOtp(request: Request, body: { email?: string }) {
body: { email, type: 'email-verification' },
})

// Send the OTP email via Resend (skipped when API key is not configured, e.g. in CI/tests)
if (process.env.RESEND_API_KEY) {
const resend = new Resend(process.env.RESEND_API_KEY)
await resend.emails.send({
Expand Down Expand Up @@ -94,7 +93,6 @@ async function handleVerifyOtp(request: Request, body: { email?: string; otp?: s

const identifier = `email-verification-otp-${email}`

// Find the verification record
const records = await payload.find({
collection: 'verifications',
where: { identifier: { equals: identifier } },
Expand Down Expand Up @@ -123,7 +121,6 @@ async function handleVerifyOtp(request: Request, body: { email?: string; otp?: s
}

if (storedOtp !== otp) {
// Increment attempt counter
await payload.update({
collection: 'verifications',
id: record.id,
Expand All @@ -132,7 +129,6 @@ async function handleVerifyOtp(request: Request, body: { email?: string; otp?: s
return NextResponse.json({ error: 'Invalid OTP' }, { status: 400 })
}

// OTP valid — mark email as pre-verified and clean up
markPreVerified(email)
await payload.delete({ collection: 'verifications', id: record.id })

Expand Down
6 changes: 2 additions & 4 deletions src/app/api/courses/[id]/completions/route.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { getCachedCourseCompletions } from '@/utilities/leaderboard'

/**
* Public projection of who completed a course (name/avatar only), consumed by
* the course-page ticker. Enrollment docs themselves stay admin-or-own.
*/
// Public projection of who completed a course (name/avatar only) — enrollment docs
// themselves stay admin-or-own.
export async function GET(
_req: Request,
{ params }: { params: Promise<{ id: string }> },
Expand Down
Loading
Loading