Phase 5: library & stats

- Full library home page: question list with per-user mastery stats,
  search/source filter, sort (A-Z, weakest first, most-seen), source
  breakdown in header, Take a test CTA.
- GET/POST /questions/{id}: view and edit question text, source, answers;
  radio-select correct answer; shows seen×/correct% stat.
- POST /questions/{id}/delete: hard delete (cascades to answers via FK).
- repo: ListQuestions supports SortWeakest/SortMostSeen via LEFT JOIN;
  added CountBySource, UpdateQuestion, UpdateAnswers, DeleteQuestion.
- render: added pct template func (correct*100/seen).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jānis Kacēns
2026-05-11 13:49:22 +03:00
parent 5199c1fa16
commit 177b4e8fd8
7 changed files with 417 additions and 14 deletions
+71
View File
@@ -0,0 +1,71 @@
{{define "content"}}
<div class="mb-5 flex items-center justify-between">
<a href="/" class="text-sm text-gray-500 hover:underline">← Library</a>
{{if .Stat}}
<p class="text-xs text-gray-500">
Seen {{.Stat.TimesSeen}}× · {{.Stat.TimesCorrect}} correct ({{pct .Stat.TimesCorrect .Stat.TimesSeen}}%)
</p>
{{else}}
<p class="text-xs text-gray-400">Not yet seen in a test</p>
{{end}}
</div>
<form method="post" action="/questions/{{.Question.ID}}" class="space-y-4">
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Question</label>
<textarea name="q_text" rows="4" required
class="w-full border border-gray-300 rounded-md px-3 py-2 text-sm shadow-sm
focus:outline-none focus:ring-2 focus:ring-blue-500 resize-y">{{.Question.Text}}</textarea>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Source</label>
<input type="text" name="source" value="{{.Question.Source}}"
class="w-full border border-gray-300 rounded-md px-3 py-1.5 text-sm shadow-sm
focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Answers</label>
<div class="space-y-2">
{{range $i, $a := .Answers}}
<div class="flex items-center gap-3">
<input type="radio" name="correct" value="{{$i}}"
{{if $a.IsCorrect}}checked{{end}}
class="text-blue-600 focus:ring-blue-500 flex-shrink-0">
<input type="text" name="a_text_{{$i}}" value="{{$a.Text}}" required
class="flex-1 border border-gray-300 rounded-md px-3 py-1.5 text-sm shadow-sm
focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
{{end}}
</div>
<p class="text-xs text-gray-400 mt-1">Select the radio button next to the correct answer.</p>
</div>
<div class="flex gap-3 pt-2">
<button type="submit"
class="flex-1 bg-blue-600 hover:bg-blue-700 text-white py-2.5 px-4 rounded-md
text-sm font-semibold shadow-sm">
Save changes
</button>
<a href="/"
class="px-4 py-2.5 border border-gray-300 rounded-md text-sm font-medium text-gray-700
hover:bg-gray-50 text-center">
Cancel
</a>
</div>
</form>
<div class="mt-6 pt-6 border-t border-gray-200">
<form method="post" action="/questions/{{.Question.ID}}/delete">
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
<button type="submit"
onclick="return confirm('Delete this question? This cannot be undone.')"
class="text-sm text-red-600 hover:text-red-800 hover:underline">
Delete this question
</button>
</form>
</div>
{{end}}