import { useState } from 'react' import { Link, useNavigate } from 'react-router-dom' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { academyClient, academyErrorMessage } from '../api/academyClient' import type { MyCourse, StudentDashboard, StudentParticipation } from '../api/types' import { Alert, Badge, Button, Card, EmptyState, ErrorState, Field, Input, Spinner, } from '../components/ui' import { useConfirm } from '../components/Confirm' import { Icon } from '../components/Icon' import { BrandMark } from '../components/BrandMark' import { formatWithZone } from '../lib/dates' import { CourseCover, ProgressBar } from './AcademyBits' import { useStudentAuth } from './StudentAuthContext' type Tab = 'kursy' | 'webinary' | 'certyfikaty' | 'dane' /** * Pulpit kursanta. * * Zakladki przelaczaja widocznosc, nie trasy: dane pobierane sa raz, jednym * zapytaniem, bo i tak pokazujemy je wszystkie naraz. */ export function StudentDashboardPage() { const { student, logout } = useStudentAuth() const navigate = useNavigate() const [tab, setTab] = useState('kursy') const dashboard = useQuery({ queryKey: ['academy', 'dashboard'], queryFn: async () => (await academyClient.get('/academy/dashboard')).data, }) const handleLogout = () => { logout() // po wylogowaniu na strone glowna, nie na formularz logowania navigate('/', { replace: true }) } return (
{student?.firstName} {student?.lastName}

Moje konto

Twoje kursy, webinary, certyfikaty i dane w jednym miejscu.

{student && !student.emailVerified && } {dashboard.isPending && } {dashboard.isError && ( )} {dashboard.data && ( <> {tab === 'kursy' && } {tab === 'webinary' && } {tab === 'certyfikaty' && } {tab === 'dane' && } )}
) } /** * Kursy kursanta: w trakcie i ukonczone. * * Przycisk prowadzi do lekcji, na ktorej kursant skonczyl, a nie na poczatek * kursu: wracajac po tygodniu nie musi szukac, gdzie przerwal. */ function CoursesTab({ items }: { items: MyCourse[] }) { if (items.length === 0) { return ( ) } const inProgress = items.filter((c) => c.status !== 'COMPLETED') const completed = items.filter((c) => c.status === 'COMPLETED') return (
{inProgress.length > 0 && (

W trakcie

{inProgress.map((c) => ( ))}
)} {completed.length > 0 && (

Ukończone

{completed.map((c) => ( ))}
)}
) } function MyCourseRow({ course }: { course: MyCourse }) { const done = course.status === 'COMPLETED' return (

{course.title}

{done && Ukończony} {/* Dostep wygasly pokazujemy wprost: inaczej kursant klika przycisk i dostaje blad, nie wiedzac dlaczego. */} {!course.accessActive && Dostęp wygasł}
{!done && course.resumeLessonTitle && (

Następna lekcja: {course.resumeLessonTitle}

)}
{course.accessActive ? ( ) : ( )}
) } /** * Przypomnienie o niepotwierdzonym adresie. * * Logowanie dziala bez potwierdzenia, ale dostep do kursow nie, wiec kursant * musi wiedziec, czego mu brakuje i jak to naprawic. */ function VerifyNotice() { const [sent, setSent] = useState(false) const resend = useMutation({ mutationFn: () => academyClient.post('/academy/resend-verification'), onSuccess: () => setSent(true), }) return (

Adres e-mail nie został jeszcze potwierdzony.

Bez potwierdzenia nie masz dostępu do materiałów kursów.{' '} {sent ? ( Wysłaliśmy nową wiadomość, sprawdź skrzynkę. ) : ( )}

) } function WebinarsTab({ items }: { items: StudentParticipation[] }) { if (items.length === 0) { return ( ) } const upcoming = items.filter((i) => i.upcoming) const past = items.filter((i) => !i.upcoming) return (
{upcoming.length > 0 && (
    {upcoming.map((item) => ( ))}
)} {past.length > 0 && (
    {past.map((item) => ( ))}
)}
) } function WebinarRow({ item }: { item: StudentParticipation }) { const cancelled = item.status === 'CANCELLED' return (
  • {item.title}

    {formatWithZone(item.startsAt, item.timezone)}

    {cancelled && rezygnacja} {!cancelled && item.attended && obecność} {!cancelled && item.upcoming && zapisany}
    {/* Link wejscia jest ten sam, ktory kursant dostal w mailu. Konto to wygoda, a nie nowy warunek dostepu: wejscie tokenem dziala dalej takze dla osob bez konta. */} {item.accessToken && (
    {item.upcoming && ( Wejdź na webinar )} Szczegóły zapisu {item.hasRecording && ( nagranie dostępne )} {item.materialCount > 0 && ( materiały: {item.materialCount} )}
    )}
  • ) } function CertificatesTab({ items }: { items: StudentDashboard['certificates'] }) { if (items.length === 0) { return ( ) } return ( {/* Jedna lista dla kursow i webinarow. Kursant nie mysli kategoriami "to bylo wydarzenie, a to kurs": ma jedno miejsce ze wszystkimi swoimi dokumentami. Zrodlo rozroznia odznaka. */}
      {items.map((item) => (
    • {item.subjectTitle}

      {item.forCourse ? 'kurs' : 'webinar'}

      nr {item.number} · {formatWithZone(item.issuedAt, 'Europe/Warsaw')}

      {item.revoked ? ( unieważniony ) : ( <> {/* Adres pobrania sklada backend, bo tylko on zna token zapisu. Wczesniej link byl budowany tutaj, bez tokenu, i pobranie konczylo sie 404 mimo zalogowania. */} {item.downloadUrl && ( Pobierz PDF )} Weryfikacja )}
    • ))}
    ) } function ProfileTab({ data }: { data: StudentDashboard }) { const confirm = useConfirm() const queryClient = useQueryClient() const { refresh } = useStudentAuth() const [form, setForm] = useState({ firstName: data.student.firstName, lastName: data.student.lastName, phone: data.student.phone ?? '', company: data.student.company ?? '', }) const [saved, setSaved] = useState(false) const [error, setError] = useState(null) const marketingEmail = data.consents.find((c) => c.type === 'MARKETING_EMAIL')?.granted ?? false const marketingSms = data.consents.find((c) => c.type === 'MARKETING_SMS')?.granted ?? false const invalidate = () => queryClient.invalidateQueries({ queryKey: ['academy', 'dashboard'] }) const saveProfile = useMutation({ mutationFn: () => academyClient.put('/academy/me', form), onSuccess: async () => { setSaved(true) setError(null) await refresh() invalidate() }, onError: (err) => setError(academyErrorMessage(err, 'Nie udało się zapisać zmian')), }) const saveConsents = useMutation({ mutationFn: (next: { marketingEmail: boolean; marketingSms: boolean }) => academyClient.put('/academy/consents', next), onSuccess: invalidate, }) const unsubscribe = useMutation({ mutationFn: () => academyClient.post('/academy/unsubscribe'), onSuccess: invalidate, }) return (
    { e.preventDefault() setSaved(false) saveProfile.mutate() }} noValidate className="space-y-4" > {error && {error}} {saved && Zmiany zostały zapisane.}
    setForm({ ...form, firstName: e.target.value })} required /> setForm({ ...form, lastName: e.target.value })} required />
    setForm({ ...form, phone: e.target.value })} /> setForm({ ...form, company: e.target.value })} />
    saveConsents.mutate({ marketingEmail: v, marketingSms })} /> saveConsents.mutate({ marketingEmail, marketingSms: v })} />
    {data.student.unsubscribed ? ( Jesteś wypisany z komunikacji marketingowej. Nadal otrzymujesz wiadomości dotyczące webinarów, na które się zapisałeś: potwierdzenia, przypomnienia i podsumowania. ) : ( <>

    Możesz wypisać się ze wszystkich wiadomości marketingowych. Powiadomienia o webinarach, na które się zapisałeś, będą wysyłane dalej.

    )}
    ) } function ConsentToggle({ label, checked, onChange, disabled, }: { label: string checked: boolean onChange: (value: boolean) => void disabled?: boolean }) { return ( ) }