Files
RuvdsTest/src/Client/Features/Quizzes/Browse/View.fs
danamir 942dfc9c1a Initial commit: standalone quiz-testing system
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.
2026-08-06 12:36:16 +03:00

144 lines
7.6 KiB
Forth
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
]
]
]
]
]
]