Skip to content

Commit 516f7a5

Browse files
committed
feat: default list view and simplify gallery cards
1 parent 513afa8 commit 516f7a5

7 files changed

Lines changed: 259 additions & 86 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
Public, statically deployed website that lists and showcases TaskBeacon task template repositories (PsyFlow/TAPS tasks) as a searchable, filterable gallery.
44

5-
- Gallery: task cards, tags, last updated, links to repo/Run/download
6-
- Task detail pages: README rendering + quick start + metadata panel
5+
- Gallery: list (default) and card views, maturity + paradigm tags, last updated, links to repo/download/clone
6+
- Task detail pages: README rendering + quick start + metadata panel + link to the README Run section
77
- Robust indexing: GitHub API repo list + README + optional `task.yaml`/`task.json`
88
- Deployment: GitHub Pages static export via GitHub Actions
99

src/app/_components/gallery-client.tsx

Lines changed: 74 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ import {
1010
} from "@/lib/task-filter";
1111
import { TagChip } from "@/components/tag-chip";
1212
import { TaskCard } from "@/components/task-card";
13+
import { TaskRow } from "@/components/task-row";
14+
import clsx from "@/components/utils/clsx";
15+
import { IconViewGrid, IconViewList } from "@/components/icons";
1316
import { formatMaturityLabel } from "@/components/maturity-badge";
1417

1518
function FacetSection({
@@ -53,15 +56,58 @@ function FacetSection({
5356
);
5457
}
5558

59+
function ViewToggle({
60+
view,
61+
setView
62+
}: {
63+
view: "list" | "cards";
64+
setView: (v: "list" | "cards") => void;
65+
}) {
66+
return (
67+
<div
68+
className="inline-flex rounded-xl border border-slate-200 bg-white p-1 shadow-sm"
69+
role="group"
70+
aria-label="Gallery view"
71+
>
72+
<button
73+
type="button"
74+
aria-pressed={view === "list"}
75+
className={clsx(
76+
"tb-focus-ring inline-flex items-center gap-2 rounded-lg px-3 py-2 text-xs font-semibold transition-colors",
77+
view === "list"
78+
? "bg-brand-700 text-white"
79+
: "text-slate-800 hover:bg-brand-50 hover:text-brand-900"
80+
)}
81+
onClick={() => setView("list")}
82+
>
83+
<IconViewList className="size-4" />
84+
List
85+
</button>
86+
<button
87+
type="button"
88+
aria-pressed={view === "cards"}
89+
className={clsx(
90+
"tb-focus-ring inline-flex items-center gap-2 rounded-lg px-3 py-2 text-xs font-semibold transition-colors",
91+
view === "cards"
92+
? "bg-brand-700 text-white"
93+
: "text-slate-800 hover:bg-brand-50 hover:text-brand-900"
94+
)}
95+
onClick={() => setView("cards")}
96+
>
97+
<IconViewGrid className="size-4" />
98+
Cards
99+
</button>
100+
</div>
101+
);
102+
}
103+
56104
export function GalleryClient({ tasks }: { tasks: TaskIndexItem[] }) {
57105
const [query, setQuery] = useState<string>("");
58106
const [selected, setSelected] = useState<SelectedFacets>(() => emptySelectedFacets());
107+
const [view, setView] = useState<"list" | "cards">("list");
59108

60109
const allMaturities = useMemo(() => facetValues(tasks, "maturity"), [tasks]);
61110
const allParadigms = useMemo(() => facetValues(tasks, "paradigm"), [tasks]);
62-
const allResponses = useMemo(() => facetValues(tasks, "response"), [tasks]);
63-
const allModalities = useMemo(() => facetValues(tasks, "modality"), [tasks]);
64-
const allLanguages = useMemo(() => facetValues(tasks, "language"), [tasks]);
65111

66112
const filtered = useMemo(() => filterTasks(tasks, query, selected), [tasks, query, selected]);
67113

@@ -105,23 +151,27 @@ export function GalleryClient({ tasks }: { tasks: TaskIndexItem[] }) {
105151
id="search"
106152
value={query}
107153
onChange={(e) => setQuery(e.target.value)}
108-
placeholder="e.g. stroop, sst, EEG"
154+
placeholder="e.g. stroop, sst"
109155
className="tb-focus-ring mt-2 w-full rounded-xl border border-slate-200 bg-white px-3 py-2 text-sm text-slate-900 placeholder:text-slate-400"
110156
/>
111-
<div className="mt-3 flex items-center justify-between gap-3">
157+
<div className="mt-3 flex flex-wrap items-center justify-between gap-3">
112158
<div className="text-sm text-slate-600">
113159
Showing <span className="font-semibold text-slate-900">{filtered.length}</span> of{" "}
114160
<span className="font-semibold text-slate-900">{tasks.length}</span>
115161
</div>
116-
{anyFilters ? (
117-
<button
118-
type="button"
119-
className="tb-focus-ring rounded-lg border border-slate-200 bg-white px-3 py-2 text-sm font-semibold text-slate-800 shadow-sm hover:border-brand-200 hover:bg-brand-50"
120-
onClick={clearAll}
121-
>
122-
Clear
123-
</button>
124-
) : null}
162+
163+
<div className="flex flex-wrap items-center gap-2">
164+
{anyFilters ? (
165+
<button
166+
type="button"
167+
className="tb-focus-ring rounded-lg border border-slate-200 bg-white px-3 py-2 text-sm font-semibold text-slate-800 shadow-sm hover:border-brand-200 hover:bg-brand-50"
168+
onClick={clearAll}
169+
>
170+
Clear
171+
</button>
172+
) : null}
173+
<ViewToggle view={view} setView={setView} />
174+
</div>
125175
</div>
126176
</section>
127177

@@ -139,27 +189,6 @@ export function GalleryClient({ tasks }: { tasks: TaskIndexItem[] }) {
139189
selected={selected}
140190
onToggle={toggleFacet}
141191
/>
142-
<FacetSection
143-
title="Response Type"
144-
facet="response"
145-
values={allResponses}
146-
selected={selected}
147-
onToggle={toggleFacet}
148-
/>
149-
<FacetSection
150-
title="Modality"
151-
facet="modality"
152-
values={allModalities}
153-
selected={selected}
154-
onToggle={toggleFacet}
155-
/>
156-
<FacetSection
157-
title="Language"
158-
facet="language"
159-
values={allLanguages}
160-
selected={selected}
161-
onToggle={toggleFacet}
162-
/>
163192
</div>
164193
</aside>
165194

@@ -182,6 +211,16 @@ export function GalleryClient({ tasks }: { tasks: TaskIndexItem[] }) {
182211
</button>
183212
</div>
184213
</div>
214+
) : view === "list" ? (
215+
<div className="space-y-3">
216+
{filtered.map((t) => (
217+
<TaskRow
218+
key={t.repo}
219+
task={t}
220+
onTagClick={(facet: TaskTagFacet, value) => toggleFacet(facet, value)}
221+
/>
222+
))}
223+
</div>
185224
) : (
186225
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
187226
{filtered.map((t) => (

src/components/icons.tsx

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,46 @@ export function IconPlay(props: { className?: string }) {
3939
</svg>
4040
);
4141
}
42+
43+
export function IconViewList(props: { className?: string }) {
44+
return (
45+
<svg
46+
viewBox="0 0 24 24"
47+
fill="none"
48+
stroke="currentColor"
49+
strokeWidth="2"
50+
strokeLinecap="round"
51+
strokeLinejoin="round"
52+
className={props.className}
53+
aria-hidden="true"
54+
>
55+
<path d="M8 6h13" />
56+
<path d="M8 12h13" />
57+
<path d="M8 18h13" />
58+
<path d="M3.5 6h.01" />
59+
<path d="M3.5 12h.01" />
60+
<path d="M3.5 18h.01" />
61+
</svg>
62+
);
63+
}
64+
65+
export function IconViewGrid(props: { className?: string }) {
66+
return (
67+
<svg
68+
viewBox="0 0 24 24"
69+
fill="none"
70+
stroke="currentColor"
71+
strokeWidth="2"
72+
strokeLinecap="round"
73+
strokeLinejoin="round"
74+
className={props.className}
75+
aria-hidden="true"
76+
>
77+
<path d="M4 4h7v7H4z" />
78+
<path d="M13 4h7v7h-7z" />
79+
<path d="M4 13h7v7H4z" />
80+
<path d="M13 13h7v7h-7z" />
81+
</svg>
82+
);
83+
}
84+

src/components/task-card.tsx

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { taskLinks } from "@/lib/task-index";
66
import { formatShortDate } from "@/lib/format";
77
import { TagChip } from "@/components/tag-chip";
88
import { MaturityBadge } from "@/components/maturity-badge";
9-
import { IconArrowRight, IconDownload, IconGithub, IconPlay } from "@/components/icons";
9+
import { IconArrowRight, IconDownload, IconGithub } from "@/components/icons";
1010

1111
function TagRow({
1212
label,
@@ -80,11 +80,7 @@ export function TaskCard({
8080

8181
<div className="mt-4 flex flex-col gap-3">
8282
<TagRow label="Paradigm" facet="paradigm" values={task.tags?.paradigm ?? []} onTagClick={onTagClick} />
83-
<TagRow label="Response" facet="response" values={task.tags?.response ?? []} onTagClick={onTagClick} />
84-
<TagRow label="Modality" facet="modality" values={task.tags?.modality ?? []} onTagClick={onTagClick} />
85-
<TagRow label="Language" facet="language" values={task.tags?.language ?? []} onTagClick={onTagClick} />
8683
</div>
87-
8884
<div className="mt-5 flex flex-wrap gap-2">
8985
<a
9086
className="tb-focus-ring inline-flex items-center gap-2 rounded-lg border border-slate-200 bg-white px-3 py-2 text-sm font-semibold text-slate-800 shadow-sm transition-colors hover:border-brand-200 hover:bg-brand-50"
@@ -95,15 +91,6 @@ export function TaskCard({
9591
<IconGithub className="size-4" />
9692
Repo
9793
</a>
98-
<a
99-
className="tb-focus-ring inline-flex items-center gap-2 rounded-lg border border-slate-200 bg-white px-3 py-2 text-sm font-semibold text-slate-800 shadow-sm transition-colors hover:border-brand-200 hover:bg-brand-50"
100-
href={links.run}
101-
target="_blank"
102-
rel="noreferrer"
103-
>
104-
<IconPlay className="size-4" />
105-
Run
106-
</a>
10794
<a
10895
className="tb-focus-ring inline-flex items-center gap-2 rounded-lg border border-slate-200 bg-white px-3 py-2 text-sm font-semibold text-slate-800 shadow-sm transition-colors hover:border-brand-200 hover:bg-brand-50"
10996
href={links.downloadZip}

src/components/task-row.tsx

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
"use client";
2+
3+
import Link from "next/link";
4+
import type { TaskIndexItem, TaskTagFacet } from "@/lib/task-index";
5+
import { taskLinks } from "@/lib/task-index";
6+
import { formatShortDate } from "@/lib/format";
7+
import { TagChip } from "@/components/tag-chip";
8+
import { MaturityBadge } from "@/components/maturity-badge";
9+
import { CopyButton } from "@/components/copy-button";
10+
import { IconArrowRight, IconDownload, IconGithub } from "@/components/icons";
11+
12+
export function TaskRow({
13+
task,
14+
onTagClick
15+
}: {
16+
task: TaskIndexItem;
17+
onTagClick?: (facet: TaskTagFacet, value: string) => void;
18+
}) {
19+
const links = taskLinks(task);
20+
const cloneCmd = `git clone ${task.html_url}.git`;
21+
22+
return (
23+
<div className="group rounded-2xl border border-slate-200 bg-white/85 p-5 shadow-sm transition-colors hover:border-brand-200 hover:bg-white">
24+
<div className="flex flex-wrap items-start justify-between gap-4">
25+
<div className="min-w-0">
26+
<div className="flex flex-wrap items-center gap-2">
27+
<Link
28+
className="tb-focus-ring rounded-md font-heading text-base font-semibold tracking-tight text-slate-900 hover:text-brand-900"
29+
href={`/tasks/${encodeURIComponent(task.repo)}`}
30+
>
31+
{task.repo}
32+
</Link>
33+
<IconArrowRight className="size-4 text-slate-400 transition-colors group-hover:text-brand-700" />
34+
{task.maturity ? <MaturityBadge maturity={task.maturity} /> : null}
35+
</div>
36+
37+
<div className="mt-2 text-sm leading-6 text-slate-700">
38+
{task.short_description || "No description provided."}
39+
</div>
40+
41+
{(task.tags?.paradigm ?? []).length > 0 ? (
42+
<div className="mt-3 flex flex-wrap items-center gap-2">
43+
<div className="text-[11px] font-semibold uppercase tracking-wide text-slate-500">
44+
Paradigm
45+
</div>
46+
<div className="flex flex-wrap gap-2">
47+
{(task.tags?.paradigm ?? []).map((v) => (
48+
<TagChip
49+
key={`paradigm:${v}`}
50+
label={v}
51+
title={`Filter by paradigm: ${v}`}
52+
onClick={onTagClick ? () => onTagClick("paradigm", v) : undefined}
53+
/>
54+
))}
55+
</div>
56+
</div>
57+
) : null}
58+
</div>
59+
60+
<div className="shrink-0 text-right">
61+
<div className="text-[11px] font-semibold uppercase tracking-wide text-slate-500">
62+
Updated
63+
</div>
64+
<div className="mt-1 text-sm font-semibold text-slate-800">
65+
{formatShortDate(task.last_updated)}
66+
</div>
67+
</div>
68+
</div>
69+
70+
<div className="mt-4 flex flex-wrap items-center gap-2">
71+
<a
72+
className="tb-focus-ring inline-flex items-center gap-2 rounded-lg border border-slate-200 bg-white px-3 py-2 text-sm font-semibold text-slate-800 shadow-sm transition-colors hover:border-brand-200 hover:bg-brand-50"
73+
href={links.repo}
74+
target="_blank"
75+
rel="noreferrer"
76+
>
77+
<IconGithub className="size-4" />
78+
Repo
79+
</a>
80+
<a
81+
className="tb-focus-ring inline-flex items-center gap-2 rounded-lg border border-slate-200 bg-white px-3 py-2 text-sm font-semibold text-slate-800 shadow-sm transition-colors hover:border-brand-200 hover:bg-brand-50"
82+
href={links.downloadZip}
83+
target="_blank"
84+
rel="noreferrer"
85+
>
86+
<IconDownload className="size-4" />
87+
Download
88+
</a>
89+
</div>
90+
91+
<div className="mt-4 flex flex-wrap items-center justify-between gap-3 rounded-xl border border-slate-200 bg-slate-50 px-3 py-2">
92+
<code className="overflow-x-auto text-xs font-semibold text-slate-800">
93+
{cloneCmd}
94+
</code>
95+
<CopyButton text={cloneCmd} label="Copy clone" />
96+
</div>
97+
</div>
98+
);
99+
}

src/data/readmes/ANT.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
# Attention Network Test (ANT)
2+
![Maturity: smoke_tested](https://img.shields.io/badge/Maturity-smoke_tested-d97706?style=for-the-badge&labelColor=c2410c)
3+
24

35
| Field | Value |
46
| -------------------- | ------------------------------------------ |

0 commit comments

Comments
 (0)