Full-stack F# (Domain/Server/Client via Fable+Elmish+Feliz), PostgreSQL persistence via Dapper, Docker Compose deployment. Student quiz-taking flow with time-limit enforcement and focus-loss tracking, Teacher question bank and quiz builder with results analytics, Admin user management.
144 lines
7.6 KiB
Forth
144 lines
7.6 KiB
Forth
module Client.Features.Quizzes.Browse.View
|
||
|
||
open Feliz
|
||
open Domain.Contracts
|
||
open Client.Shared
|
||
open Client.Features.Quizzes.Browse.Types
|
||
|
||
let private attemptRow (a: MyAttemptSummary) =
|
||
Html.li [
|
||
prop.key (string a.AttemptId)
|
||
prop.className "attempt-row"
|
||
prop.children [
|
||
Html.span [
|
||
prop.className "tag-mono"
|
||
prop.text (a.StartedAt.ToLocalTime().ToString("dd.MM.yyyy HH:mm"))
|
||
]
|
||
Html.span [
|
||
prop.className "score-value"
|
||
prop.text (sprintf "%s / %s" (Format.points a.Score) (Format.points a.MaxScore))
|
||
]
|
||
match a.Passed with
|
||
| Some true -> Html.span [ prop.className "grade-stamp passed"; prop.text "Пройден" ]
|
||
| Some false -> Html.span [ prop.className "grade-stamp failed"; prop.text "Не пройден" ]
|
||
| None -> Html.none
|
||
]
|
||
]
|
||
|
||
let private resultsPanel (model: Model) =
|
||
Html.div [
|
||
prop.className "results-panel"
|
||
prop.children [
|
||
match model.ResultsError with
|
||
| Some err -> Html.p [ prop.className "error"; prop.text err ]
|
||
| None -> Html.none
|
||
if model.ResultsLoading then
|
||
Html.p "Загрузка…"
|
||
elif model.Results.IsEmpty then
|
||
Html.p "Нет завершённых попыток"
|
||
else
|
||
Html.ul [ prop.children [ for a in model.Results -> attemptRow a ] ]
|
||
]
|
||
]
|
||
|
||
let view (model: Model) (dispatch: Msg -> unit) =
|
||
Html.div [
|
||
prop.className "quiz-list-page"
|
||
prop.children [
|
||
Html.h2 "Доступные тесты"
|
||
match model.Error with
|
||
| Some err -> Html.p [ prop.className "error"; prop.text err ]
|
||
| None -> Html.none
|
||
if model.Loading then
|
||
Html.p "Загрузка…"
|
||
elif model.Quizzes.IsEmpty then
|
||
Html.p "Тесты недоступны"
|
||
else
|
||
Html.ul [
|
||
prop.children [
|
||
for quiz in model.Quizzes ->
|
||
let remainingAttempts =
|
||
quiz.MaxAttempts |> Option.map (fun m -> System.Math.Max(0, m - quiz.AttemptsCount))
|
||
|
||
Html.li [
|
||
prop.key (string quiz.Id)
|
||
prop.className "quiz-card"
|
||
prop.children [
|
||
Html.h3 quiz.Title
|
||
Html.p quiz.Description
|
||
Html.div [
|
||
prop.className "meta-row"
|
||
prop.children [
|
||
Html.div [
|
||
prop.className "meta-item"
|
||
prop.children [
|
||
Html.span [ prop.className "meta-label"; prop.text "Баллов" ]
|
||
Html.span [
|
||
prop.className "meta-value"
|
||
prop.text (Format.points quiz.TotalPoints)
|
||
]
|
||
]
|
||
]
|
||
match quiz.TimeLimitMinutes with
|
||
| Some minutes ->
|
||
Html.div [
|
||
prop.className "meta-item"
|
||
prop.children [
|
||
Html.span [ prop.className "meta-label"; prop.text "Лимит времени" ]
|
||
Html.span [
|
||
prop.className "meta-value"
|
||
prop.text (sprintf "%d мин" minutes)
|
||
]
|
||
]
|
||
]
|
||
| None -> Html.none
|
||
match quiz.MaxAttempts, remainingAttempts with
|
||
| Some maxAttempts, Some remaining ->
|
||
Html.div [
|
||
prop.className "meta-item"
|
||
prop.children [
|
||
Html.span [ prop.className "meta-label"; prop.text "Осталось попыток" ]
|
||
Html.span [
|
||
prop.className "meta-value"
|
||
prop.text (sprintf "%d из %d" remaining maxAttempts)
|
||
]
|
||
]
|
||
]
|
||
| _ -> Html.none
|
||
]
|
||
]
|
||
if quiz.AlreadyPassed then
|
||
Html.span [ prop.className "status-badge active"; prop.text "Тест пройден успешно" ]
|
||
elif remainingAttempts = Some 0 then
|
||
Html.p [ prop.className "hint"; prop.text "Попытки исчерпаны" ]
|
||
else
|
||
Html.button [
|
||
prop.disabled (model.StartingQuizId = Some quiz.Id)
|
||
prop.onClick (fun _ -> dispatch (Start quiz.Id))
|
||
prop.text (
|
||
if model.StartingQuizId = Some quiz.Id then
|
||
"Запуск…"
|
||
else
|
||
"Начать тест"
|
||
)
|
||
]
|
||
if quiz.AttemptsCount > 0 then
|
||
Html.button [
|
||
prop.type'.button
|
||
prop.onClick (fun _ -> dispatch (ToggleResults quiz.Id))
|
||
prop.text (
|
||
if model.ExpandedResultsFor = Some quiz.Id then
|
||
"Скрыть результаты"
|
||
else
|
||
sprintf "Посмотреть результаты (%d)" quiz.AttemptsCount
|
||
)
|
||
]
|
||
if model.ExpandedResultsFor = Some quiz.Id then
|
||
resultsPanel model
|
||
]
|
||
]
|
||
]
|
||
]
|
||
]
|
||
]
|