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 .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
"Berlín",
"codegen",
"dtos",
"linecap",
"linejoin",
"fgutierrez",
"formisch",
"katex",
Expand Down
16 changes: 15 additions & 1 deletion apps/client/src/app.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,21 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon-light.png" />
<!-- El SVG cambia de color con el tema del navegador; los PNG cubren a quien no lo soporta. -->
<link rel="icon" href="/favicon-light.png" media="(prefers-color-scheme: light)" />
<link rel="icon" href="/favicon-dark.png" media="(prefers-color-scheme: dark)" />
<link rel="icon" href="/favicon.svg" sizes="any" type="image/svg+xml" />
<script>
// Fija el tema antes del primer pintado para evitar el destello claro. La lógica de
// alternancia vive en `lib/shared/theme.svelte.ts`.
try {
const stored = localStorage.getItem("theme")
const dark = stored
? stored === "dark"
: window.matchMedia("(prefers-color-scheme: dark)").matches
document.documentElement.classList.toggle("dark", dark)
} catch {}
</script>
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
Expand Down
194 changes: 98 additions & 96 deletions apps/client/src/lib/attempts/components/AttemptResultReview.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import { DateValue } from "$lib/shared/value-objects/date.value"
import { GradeValue } from "$lib/shared/value-objects/grade.value"
import Markdown from "$lib/shared/components/Markdown.svelte"
import { User, ChartColumn, CircleCheck, CircleX, Trophy } from "lucide-svelte"
import { User, ChartColumn, Check, X, Minus, Trophy } from "lucide-svelte"

let { result }: { result: AttemptResult } = $props()

Expand All @@ -27,58 +27,47 @@
{ level: "high", label: "Alta" },
]

const getOptionClass = (
optionIndex: number,
correctAnswerIndex: number,
answerIndex: number | null
) => {
if (optionIndex === correctAnswerIndex) {
return "border-emerald-700 bg-emerald-50 text-emerald-900"
}

if (
answerIndex !== null &&
optionIndex === answerIndex &&
answerIndex !== correctAnswerIndex
) {
return "border-red-700 bg-red-50 text-red-900"
}

if (answerIndex !== null && optionIndex === answerIndex) {
return "border-black bg-black text-white"
}
const resultOf = (question: AttemptResult["questions"][number]) => {
if (question.answerIndex === null) return "none"
return question.isCorrect ? "ok" : "bad"
}

return "border-zinc-300 bg-white text-black"
const resultStyles = {
ok: "bg-(--ok)/8 text-(--ok)",
bad: "bg-(--danger)/8 text-(--danger)",
none: "bg-amber-700/10 text-amber-800",
}

const resultLabels = { ok: "Correcta", bad: "Incorrecta", none: "Sin responder" }
</script>

<section class="flex h-full min-h-0 flex-col gap-4">
<div class="grid gap-3 sm:grid-cols-[1.5fr_1fr_1fr_1fr]">
<div class="panel-muted p-3 sm:p-4">
<div class="grid grid-cols-2 gap-3 sm:grid-cols-[1.5fr_1fr_1fr_1fr]">
<div class="panel-surface col-span-2 p-3 sm:col-span-1 sm:p-4">
<p class="m-0 flex items-center gap-2 text-xs font-medium text-zinc-700">
<User size={13} /> Estudiante
</p>
<p class="mt-1 mb-0 text-lg font-semibold text-black">
{result.userName}
</p>
</div>
<div class="panel-muted p-3 sm:p-4">
<div class="panel-surface p-3 sm:p-4">
<p class="m-0 flex items-center gap-2 text-xs font-medium text-zinc-700">
<Trophy size={13} /> Nota
</p>
<p class="mt-1 mb-0 text-lg font-semibold text-black">
{GradeValue.format(result.grade)}
</p>
</div>
<div class="panel-muted p-3 sm:p-4">
<div class="panel-surface p-3 sm:p-4">
<p class="m-0 flex items-center gap-2 text-xs font-medium text-zinc-700">
<ChartColumn size={13} /> Puntos
</p>
<p class="mt-1 mb-0 text-lg font-semibold text-black">
{result.score} / {result.maxScore}
</p>
</div>
<div class="panel-muted p-3 sm:p-4">
<div class="panel-surface col-span-2 p-3 sm:col-span-1 sm:p-4">
<p class="m-0 flex items-center gap-2 text-xs font-medium text-zinc-700">Enviado</p>
<p class="mt-1 mb-0 text-lg font-semibold text-black">
{DateValue.format(result.submittedAt)}
Expand All @@ -88,63 +77,84 @@

<div class="min-h-0 space-y-4 overflow-auto pr-1">
{#each result.questions as question, index (question.questionId)}
<article class="panel-muted p-4 sm:p-5">
<div class="flex items-start justify-between gap-3">
<div class="min-w-0 flex-1">
<div class="flex items-start gap-2">
<span class="shrink-0 font-medium text-black">{index + 1}.</span>
<div class="min-w-0 flex-1">
<Markdown content={question.question} />
</div>
{@const outcome = resultOf(question)}
<article class="panel-surface p-4 sm:p-5">
<div
class="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between sm:gap-3"
>
<div class="flex min-w-0 flex-1 items-start gap-2">
<span class="shrink-0 font-medium text-black">{index + 1}.</span>
<div class="min-w-0 flex-1">
<Markdown content={question.question} />
</div>
</div>
{#if question.isCorrect}
<span
class="inline-flex shrink-0 items-center gap-1 text-sm font-medium text-emerald-700"
>
<CircleCheck size={14} /> Correcta
</span>
{:else}
<span
class="inline-flex shrink-0 items-center gap-1 text-sm font-medium text-red-700"
>
<CircleX size={14} /> Incorrecta
</span>
{/if}
<span
class={`inline-flex h-6.5 shrink-0 items-center gap-1.5 self-start rounded-full px-2.5 text-[13px] font-semibold whitespace-nowrap ${resultStyles[outcome]}`}
>
{#if outcome === "ok"}
<Check size={14} />
{:else if outcome === "bad"}
<X size={14} />
{:else}
<Minus size={14} />
{/if}
{resultLabels[outcome]}
{#if outcome !== "none"}
<span class="opacity-50">·</span>
<span class="tabular-nums"
>{formatSignedPoints(question.awardedPoints)} pt</span
>
{/if}
</span>
</div>

<div class="mt-4 grid gap-2.5">
<div class="mt-4 grid gap-1.5">
{#each question.options as option, optionIndex (`${question.questionId}-${optionIndex}`)}
{@const isSelected =
question.answerIndex !== null && optionIndex === question.answerIndex}
{@const isCorrect = optionIndex === question.correctAnswerIndex}
{@const isSelected = optionIndex === question.answerIndex}
{@const isWrong = isSelected && !isCorrect}
<div
class={`rounded-sm border px-4 py-3 text-left text-base leading-relaxed ${getOptionClass(
optionIndex,
question.correctAnswerIndex,
question.answerIndex
)}`}
class={`flex flex-wrap items-center gap-x-3 gap-y-1 rounded-sm border px-3 py-2.5 sm:flex-nowrap ${
isCorrect
? "border-transparent bg-(--ok)/8"
: isWrong
? "border-transparent bg-(--danger)/8"
: "border-zinc-200"
}`}
>
<div
class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between sm:gap-3"
<span
class={`grid size-5 shrink-0 place-items-center rounded-full border-[1.5px] ${
isCorrect && isSelected
? "border-(--ok) bg-(--ok) text-white"
: isCorrect
? "border-(--ok) text-(--ok)"
: isWrong
? "border-(--danger) bg-(--danger) text-white"
: "border-zinc-300"
}`}
>
<div class="min-w-0 flex-1">
<Markdown content={option} inline />
</div>
{#if isSelected}
<div class="flex flex-wrap gap-2 sm:shrink-0">
<span
class="inline-flex h-6 min-w-12 shrink-0 items-center justify-center rounded-sm border border-zinc-300 bg-white/90 px-2 text-xs font-semibold text-zinc-700"
>
{formatSignedPoints(question.awardedPoints)}pt
</span>
<span
class="inline-flex h-6 min-w-16 shrink-0 items-center justify-center rounded-sm border border-blue-500 bg-blue-50 px-2 text-xs font-semibold text-blue-800"
>
Alternativa seleccionada
</span>
</div>
{#if isCorrect}
<Check size={12} strokeWidth={3} />
{:else if isWrong}
<X size={12} strokeWidth={3} />
{/if}
</span>
<div class="min-w-0 flex-1 text-base leading-relaxed text-black">
<Markdown content={option} inline />
</div>
{#if isSelected || isCorrect}
<span
class={`basis-full pl-8 text-xs font-semibold sm:basis-auto sm:pl-0 ${
isWrong
? "text-(--danger)"
: isCorrect
? "text-(--ok)"
: "text-zinc-600"
}`}
>
{isSelected ? "Tu respuesta" : "Respuesta correcta"}
</span>
{/if}
</div>
{/each}
</div>
Expand All @@ -159,31 +169,23 @@
{/if}

{#if question.certaintyLevel}
<div class="mt-4 grid gap-2 border-t border-zinc-200 pt-4">
<p class="m-0 text-sm font-medium text-black">Nivel de certeza marcado</p>
<div class="grid gap-2 sm:grid-cols-3">
{#each certaintyLevels as item (item.level)}
<div
class={`rounded-sm border px-4 py-3 text-left transition ${
question.certaintyLevel === item.level
? "border-black bg-black text-white shadow-[0_10px_20px_rgba(0,0,0,0.08)]"
: "border-zinc-300 bg-white text-black"
}`}
>
<span class="block text-sm font-medium">
{item.label}
</span>
</div>
{@const filled = certaintyLevels.findIndex(
(item) => item.level === question.certaintyLevel
)}
<div
class="mt-4 flex items-center gap-2.5 border-t border-zinc-200 pt-4 text-sm text-zinc-600"
>
Nivel de certeza marcado
<span class="flex gap-0.5" aria-hidden="true">
{#each certaintyLevels as item, i (item.level)}
<span
class={`h-1.5 w-4 rounded-full ${i <= filled ? "bg-zinc-800" : "bg-zinc-200"}`}
></span>
{/each}
</div>
</span>
<span class="font-medium text-black">{certaintyLevels[filled]?.label}</span>
</div>
{/if}

{#if question.answerIndex === null}
<p class="mt-4 mb-0 text-sm font-bold text-amber-700">
Sin respuesta en esta pregunta.
</p>
{/if}
</article>
{/each}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

{#if open}
<div
class="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4"
class="fixed inset-0 z-50 flex items-center justify-center bg-(--scrim) p-4"
transition:fade={{ duration: 150 }}
role="dialog"
aria-modal="true"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@
{#each question.options as _, optionIndex (optionIndex)}
<div class="flex items-center gap-2">
<input
class="shrink-0"
class="[--check:var(--ok)]"
type="radio"
name={`correct-${question.key}`}
checked={question.answerIndex === optionIndex}
Expand Down
4 changes: 2 additions & 2 deletions apps/client/src/lib/banks/components/BankUploadModal.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ La capital de Francia es París desde hace siglos.

{#if open}
<div
class="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4"
class="fixed inset-0 z-50 flex items-center justify-center bg-(--scrim) p-4"
role="dialog"
aria-modal="true"
tabindex="-1"
Expand Down Expand Up @@ -223,7 +223,7 @@ La capital de Francia es París desde hace siglos.

{#if showFormatModal}
<div
class="fixed inset-0 z-60 flex items-center justify-center bg-black/40 p-4"
class="fixed inset-0 z-60 flex items-center justify-center bg-(--scrim) p-4"
role="dialog"
aria-modal="true"
tabindex="-1"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@

{#if open}
<div
class="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4"
class="fixed inset-0 z-50 flex items-center justify-center bg-(--scrim) p-4"
role="dialog"
aria-modal="true"
tabindex="-1"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@

{#if open}
<div
class="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4"
class="fixed inset-0 z-50 flex items-center justify-center bg-(--scrim) p-4"
role="dialog"
aria-modal="true"
tabindex="-1"
Expand Down
12 changes: 6 additions & 6 deletions apps/client/src/lib/quizzes/components/CertaintyTableView.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,19 @@
]
</script>

<div class="overflow-x-auto rounded-[4px] border border-zinc-200">
<table class="w-full border-collapse text-xs">
<thead class="text-zinc-500">
<tr class="border-b border-zinc-200">
<div class="panel-surface overflow-x-auto">
<table class="w-full border-collapse text-sm">
<thead class="table-head">
<tr>
<th class="px-3 py-2 text-left font-medium">Nivel</th>
<th class="px-3 py-2 text-left font-medium">Correcta</th>
<th class="px-3 py-2 text-left font-medium">Incorrecta</th>
</tr>
</thead>
<tbody>
{#each levels as level (level.key)}
<tr class="border-b border-zinc-200/70 last:border-b-0">
<td class="px-3 py-2 text-zinc-700">{level.label}</td>
<tr class="border-t border-zinc-200">
<td class="px-3 py-2 text-zinc-800">{level.label}</td>
<td class="px-3 py-2 text-zinc-700">{table.get(level.key).correct}</td>
<td class="px-3 py-2 text-zinc-700">{table.get(level.key).incorrect}</td>
</tr>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@

{#if open}
<div
class="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4"
class="fixed inset-0 z-50 flex items-center justify-center bg-(--scrim) p-4"
role="dialog"
aria-modal="true"
tabindex="-1"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@

{#if open}
<div
class="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4"
class="fixed inset-0 z-50 flex items-center justify-center bg-(--scrim) p-4"
role="dialog"
aria-modal="true"
tabindex="-1"
Expand Down
Loading
Loading