177b4e8fd8
- 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>
80 lines
3.1 KiB
HTML
80 lines
3.1 KiB
HTML
{{define "content"}}
|
||
<div class="mb-5 flex items-baseline justify-between">
|
||
<h1 class="text-2xl font-bold text-gray-800">Library</h1>
|
||
<a href="/upload" class="text-sm text-blue-600 hover:underline">+ Upload</a>
|
||
</div>
|
||
|
||
{{if eq .TotalQ 0}}
|
||
<div class="text-center py-16 text-gray-400">
|
||
<p class="text-lg">No questions yet.</p>
|
||
<a href="/upload" class="mt-4 inline-block text-blue-600 hover:underline text-sm">Upload a document to get started</a>
|
||
</div>
|
||
{{else}}
|
||
|
||
<div class="mb-5 text-sm text-gray-500">
|
||
{{.TotalQ}} question{{if ne .TotalQ 1}}s{{end}} · {{.TotalA}} answer{{if ne .TotalA 1}}s{{end}}
|
||
{{if .SourceStats}}
|
||
<span class="mx-1">·</span>
|
||
{{range $i, $s := .SourceStats}}{{if $i}}<span class="mx-0.5 text-gray-300">|</span>{{end}}<span>{{$s.Source}} ({{$s.Count}})</span>{{end}}
|
||
{{end}}
|
||
</div>
|
||
|
||
<form method="get" action="/" class="flex flex-wrap gap-2 mb-4">
|
||
<input type="text" name="q" value="{{.Search}}" placeholder="Search…"
|
||
class="flex-1 min-w-0 border border-gray-300 rounded-md px-3 py-1.5 text-sm
|
||
focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||
{{if .SourceStats}}
|
||
<select name="source" onchange="this.form.submit()"
|
||
class="border border-gray-300 rounded-md px-2 py-1.5 text-sm
|
||
focus:outline-none focus:ring-2 focus:ring-blue-500 bg-white">
|
||
<option value="">All sources</option>
|
||
{{range .SourceStats}}
|
||
<option value="{{.Source}}" {{if eq .Source $.SelectedSource}}selected{{end}}>{{.Source}}</option>
|
||
{{end}}
|
||
</select>
|
||
{{end}}
|
||
<select name="sort" onchange="this.form.submit()"
|
||
class="border border-gray-300 rounded-md px-2 py-1.5 text-sm
|
||
focus:outline-none focus:ring-2 focus:ring-blue-500 bg-white">
|
||
<option value="alpha" {{if eq .Sort "alpha"}}selected{{end}}>A–Z</option>
|
||
<option value="weakest" {{if eq .Sort "weakest"}}selected{{end}}>Weakest first</option>
|
||
<option value="seen" {{if eq .Sort "seen"}}selected{{end}}>Most seen</option>
|
||
</select>
|
||
<button type="submit"
|
||
class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-1.5 rounded-md text-sm font-medium">
|
||
Search
|
||
</button>
|
||
</form>
|
||
|
||
{{if .Questions}}
|
||
<div class="space-y-2">
|
||
{{range .Questions}}
|
||
<a href="/questions/{{.Q.ID}}"
|
||
class="block bg-white border border-gray-200 rounded-lg px-4 py-3 hover:border-blue-300 hover:shadow-sm transition-all">
|
||
<div class="flex items-start justify-between gap-4">
|
||
<p class="text-sm text-gray-800 line-clamp-2">{{.Q.Text}}</p>
|
||
<span class="text-xs text-gray-400 whitespace-nowrap mt-0.5 flex-shrink-0">
|
||
{{if .Stat}}{{.Stat.TimesSeen}}× · {{pct .Stat.TimesCorrect .Stat.TimesSeen}}%{{else}}—{{end}}
|
||
</span>
|
||
</div>
|
||
{{if .Q.Source}}
|
||
<p class="text-xs text-gray-400 mt-1">{{.Q.Source}}</p>
|
||
{{end}}
|
||
</a>
|
||
{{end}}
|
||
</div>
|
||
{{else}}
|
||
<p class="text-center py-8 text-gray-400 text-sm">No questions match your filter.</p>
|
||
{{end}}
|
||
|
||
<div class="mt-6 text-center">
|
||
<a href="/test/new"
|
||
class="inline-block bg-blue-600 hover:bg-blue-700 text-white px-8 py-2.5 rounded-md
|
||
text-sm font-semibold shadow-sm">
|
||
Take a test
|
||
</a>
|
||
</div>
|
||
|
||
{{end}}
|
||
{{end}}
|