feat: Rewrote TopicsBrowse.tsx from vertical accordion to responsive 2-…
- "frontend/src/pages/TopicsBrowse.tsx" - "frontend/src/App.css" GSD-Task: S05/T02
This commit is contained in:
parent
3f3fe065f8
commit
75332343cb
5 changed files with 261 additions and 97 deletions
|
|
@ -15,7 +15,7 @@ Steps:
|
||||||
- Estimate: 15m
|
- Estimate: 15m
|
||||||
- Files: config/canonical_tags.yaml (on ub01 via SSH), frontend/src/App.css
|
- Files: config/canonical_tags.yaml (on ub01 via SSH), frontend/src/App.css
|
||||||
- Verify: curl -s http://ub01:8096/api/v1/topics | python3 -c "import sys,json; d=json.load(sys.stdin); assert len(d)==7, f'Expected 7 categories, got {len(d)}'; names=[c['name'] for c in d]; assert 'Music Theory' in names, f'Music Theory not found in {names}'; print('PASS: 7 categories including Music Theory')" && cd frontend && npx grep-cli --no-error 'music-theory' src/App.css || grep -q 'music-theory' frontend/src/App.css && echo 'PASS: badge CSS present'
|
- Verify: curl -s http://ub01:8096/api/v1/topics | python3 -c "import sys,json; d=json.load(sys.stdin); assert len(d)==7, f'Expected 7 categories, got {len(d)}'; names=[c['name'] for c in d]; assert 'Music Theory' in names, f'Music Theory not found in {names}'; print('PASS: 7 categories including Music Theory')" && cd frontend && npx grep-cli --no-error 'music-theory' src/App.css || grep -q 'music-theory' frontend/src/App.css && echo 'PASS: badge CSS present'
|
||||||
- [ ] **T02: Redesign TopicsBrowse.tsx to card grid layout with updated CSS** — Rewrite `TopicsBrowse.tsx` from the current vertical accordion list to a responsive card grid layout, and replace the topics CSS section in `App.css` with card-based styles.
|
- [x] **T02: Rewrote TopicsBrowse.tsx from vertical accordion to responsive 2-column card grid with category color accents, summary stats, and expand/collapse toggles** — Rewrite `TopicsBrowse.tsx` from the current vertical accordion list to a responsive card grid layout, and replace the topics CSS section in `App.css` with card-based styles.
|
||||||
|
|
||||||
The card layout should:
|
The card layout should:
|
||||||
- Use CSS grid: 2 columns on desktop (min-width > 768px), 1 column on mobile
|
- Use CSS grid: 2 columns on desktop (min-width > 768px), 1 column on mobile
|
||||||
|
|
|
||||||
22
.gsd/milestones/M006/slices/S05/tasks/T01-VERIFY.json
Normal file
22
.gsd/milestones/M006/slices/S05/tasks/T01-VERIFY.json
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"taskId": "T01",
|
||||||
|
"unitId": "M006/S05/T01",
|
||||||
|
"timestamp": 1774871058689,
|
||||||
|
"passed": true,
|
||||||
|
"discoverySource": "task-plan",
|
||||||
|
"checks": [
|
||||||
|
{
|
||||||
|
"command": "cd frontend",
|
||||||
|
"exitCode": 0,
|
||||||
|
"durationMs": 6,
|
||||||
|
"verdict": "pass"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "echo 'PASS: badge CSS present'",
|
||||||
|
"exitCode": 0,
|
||||||
|
"durationMs": 6,
|
||||||
|
"verdict": "pass"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
81
.gsd/milestones/M006/slices/S05/tasks/T02-SUMMARY.md
Normal file
81
.gsd/milestones/M006/slices/S05/tasks/T02-SUMMARY.md
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
---
|
||||||
|
id: T02
|
||||||
|
parent: S05
|
||||||
|
milestone: M006
|
||||||
|
provides: []
|
||||||
|
requires: []
|
||||||
|
affects: []
|
||||||
|
key_files: ["frontend/src/pages/TopicsBrowse.tsx", "frontend/src/App.css"]
|
||||||
|
key_decisions: ["Used 3px colored left border + small colored dot for category visual differentiation — subtler than full colored header, maintains dark theme cohesion", "Computed totalTechniques client-side via sub_topics.reduce() — no API changes needed"]
|
||||||
|
patterns_established: []
|
||||||
|
drill_down_paths: []
|
||||||
|
observability_surfaces: []
|
||||||
|
duration: ""
|
||||||
|
verification_result: "Frontend build passes (800ms, no errors). Live CSS and JS bundles contain topic-card class references. API returns 7 categories. Browser verification confirms 2-column desktop grid, 1-column mobile, filter narrows to matching cards, expand/collapse toggles work correctly."
|
||||||
|
completed_at: 2026-03-30T11:48:43.282Z
|
||||||
|
blocker_discovered: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# T02: Rewrote TopicsBrowse.tsx from vertical accordion to responsive 2-column card grid with category color accents, summary stats, and expand/collapse toggles
|
||||||
|
|
||||||
|
> Rewrote TopicsBrowse.tsx from vertical accordion to responsive 2-column card grid with category color accents, summary stats, and expand/collapse toggles
|
||||||
|
|
||||||
|
## What Happened
|
||||||
|
---
|
||||||
|
id: T02
|
||||||
|
parent: S05
|
||||||
|
milestone: M006
|
||||||
|
key_files:
|
||||||
|
- frontend/src/pages/TopicsBrowse.tsx
|
||||||
|
- frontend/src/App.css
|
||||||
|
key_decisions:
|
||||||
|
- Used 3px colored left border + small colored dot for category visual differentiation — subtler than full colored header, maintains dark theme cohesion
|
||||||
|
- Computed totalTechniques client-side via sub_topics.reduce() — no API changes needed
|
||||||
|
duration: ""
|
||||||
|
verification_result: passed
|
||||||
|
completed_at: 2026-03-30T11:48:43.283Z
|
||||||
|
blocker_discovered: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# T02: Rewrote TopicsBrowse.tsx from vertical accordion to responsive 2-column card grid with category color accents, summary stats, and expand/collapse toggles
|
||||||
|
|
||||||
|
**Rewrote TopicsBrowse.tsx from vertical accordion to responsive 2-column card grid with category color accents, summary stats, and expand/collapse toggles**
|
||||||
|
|
||||||
|
## What Happened
|
||||||
|
|
||||||
|
Replaced the single-column accordion layout with a CSS grid card layout. Each card shows a colored left border and dot (via existing badge custom properties), category name, description, sub-topic + technique count stats line, and expand/collapse toggle. Grid is 2 columns on desktop, 1 on mobile. CSS rewritten from .topics-list/.topic-category* to .topics-grid/.topic-card* classes. All existing functionality preserved (filter, expand/collapse, sub-topic navigation). Deployed to ub01 and verified in browser at both desktop and mobile viewports.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
Frontend build passes (800ms, no errors). Live CSS and JS bundles contain topic-card class references. API returns 7 categories. Browser verification confirms 2-column desktop grid, 1-column mobile, filter narrows to matching cards, expand/collapse toggles work correctly.
|
||||||
|
|
||||||
|
## Verification Evidence
|
||||||
|
|
||||||
|
| # | Command | Exit Code | Verdict | Duration |
|
||||||
|
|---|---------|-----------|---------|----------|
|
||||||
|
| 1 | `cd frontend && npm run build` | 0 | ✅ pass | 800ms |
|
||||||
|
| 2 | `curl CSS bundle | grep topic-card` | 0 | ✅ pass | 200ms |
|
||||||
|
| 3 | `curl JS bundle | grep topic-card` | 0 | ✅ pass | 200ms |
|
||||||
|
| 4 | `curl API /topics | assert len==7` | 0 | ✅ pass | 300ms |
|
||||||
|
| 5 | `Browser: desktop 2-col grid + mobile 1-col + filter + toggle` | 0 | ✅ pass | 5000ms |
|
||||||
|
|
||||||
|
|
||||||
|
## Deviations
|
||||||
|
|
||||||
|
Plan verification command used grep on HTML for topic-category classes, but SPA serves class names in JS bundles — adapted to check bundle contents instead.
|
||||||
|
|
||||||
|
## Known Issues
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Files Created/Modified
|
||||||
|
|
||||||
|
- `frontend/src/pages/TopicsBrowse.tsx`
|
||||||
|
- `frontend/src/App.css`
|
||||||
|
|
||||||
|
|
||||||
|
## Deviations
|
||||||
|
Plan verification command used grep on HTML for topic-category classes, but SPA serves class names in JS bundles — adapted to check bundle contents instead.
|
||||||
|
|
||||||
|
## Known Issues
|
||||||
|
None.
|
||||||
|
|
@ -1839,11 +1839,11 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ══════════════════════════════════════════════════════════════════════════════
|
/* ══════════════════════════════════════════════════════════════════════════════
|
||||||
TOPICS BROWSE
|
TOPICS BROWSE — Card Grid Layout
|
||||||
══════════════════════════════════════════════════════════════════════════════ */
|
══════════════════════════════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
.topics-browse {
|
.topics-browse {
|
||||||
max-width: 56rem;
|
max-width: 64rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.topics-browse__title {
|
.topics-browse__title {
|
||||||
|
|
@ -1869,7 +1869,7 @@ body {
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
color: var(--color-text-primary);
|
color: var(--color-text-primary);
|
||||||
background: var(--color-bg-input);
|
background: var(--color-bg-input);
|
||||||
margin-bottom: 1.25rem;
|
margin-bottom: 1.5rem;
|
||||||
transition: border-color 0.15s, box-shadow 0.15s;
|
transition: border-color 0.15s, box-shadow 0.15s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1879,67 +1879,92 @@ body {
|
||||||
box-shadow: 0 0 0 2px var(--color-accent-focus);
|
box-shadow: 0 0 0 2px var(--color-accent-focus);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Topics hierarchy ─────────────────────────────────────────────────────── */
|
/* ── Card grid ────────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
.topics-list {
|
.topics-grid {
|
||||||
display: flex;
|
display: grid;
|
||||||
flex-direction: column;
|
grid-template-columns: repeat(2, 1fr);
|
||||||
gap: 0.75rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.topic-category {
|
.topic-card {
|
||||||
background: var(--color-bg-surface);
|
background: var(--color-bg-surface);
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
|
border-left: 3px solid var(--color-border);
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-shadow: 0 1px 3px var(--color-shadow);
|
box-shadow: 0 1px 3px var(--color-shadow);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.topic-category__header {
|
.topic-card__body {
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topic-card__name {
|
||||||
|
font-size: 1.0625rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--color-text-primary);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.625rem;
|
gap: 0.5rem;
|
||||||
width: 100%;
|
margin: 0;
|
||||||
padding: 0.875rem 1.25rem;
|
}
|
||||||
|
|
||||||
|
.topic-card__dot {
|
||||||
|
display: inline-block;
|
||||||
|
width: 0.5rem;
|
||||||
|
height: 0.5rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topic-card__desc {
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
line-height: 1.45;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topic-card__stats {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.375rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topic-card__stats-sep {
|
||||||
|
color: var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.topic-card__toggle {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
margin-top: 0.375rem;
|
||||||
|
padding: 0.25rem 0;
|
||||||
border: none;
|
border: none;
|
||||||
background: none;
|
background: none;
|
||||||
cursor: pointer;
|
|
||||||
text-align: left;
|
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
transition: background 0.15s;
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-accent);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.15s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.topic-category__header:hover {
|
.topic-card__toggle:hover {
|
||||||
background: var(--color-bg-surface-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
.topic-category__chevron {
|
|
||||||
font-size: 0.625rem;
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
flex-shrink: 0;
|
|
||||||
width: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.topic-category__name {
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--color-text-primary);
|
color: var(--color-text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.topic-category__desc {
|
/* ── Sub-topics inside card ───────────────────────────────────────────────── */
|
||||||
font-size: 0.8125rem;
|
|
||||||
color: var(--color-text-secondary);
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.topic-category__count {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
white-space: nowrap;
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Sub-topics ───────────────────────────────────────────────────────────── */
|
|
||||||
|
|
||||||
.topic-subtopics {
|
.topic-subtopics {
|
||||||
border-top: 1px solid var(--color-border);
|
border-top: 1px solid var(--color-border);
|
||||||
|
|
@ -1949,10 +1974,10 @@ body {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 0.625rem 1.25rem 0.625rem 2.75rem;
|
padding: 0.5rem 1.25rem;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
font-size: 0.875rem;
|
font-size: 0.8125rem;
|
||||||
transition: background 0.1s;
|
transition: background 0.1s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1974,7 +1999,7 @@ body {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.375rem;
|
gap: 0.375rem;
|
||||||
font-size: 0.75rem;
|
font-size: 0.6875rem;
|
||||||
color: var(--color-text-muted);
|
color: var(--color-text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2037,12 +2062,16 @@ body {
|
||||||
font-size: 1.375rem;
|
font-size: 1.375rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.topic-category__desc {
|
.topics-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topic-card__desc {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.topic-subtopic {
|
.topic-subtopic {
|
||||||
padding-left: 2rem;
|
padding-left: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,23 @@
|
||||||
/**
|
/**
|
||||||
* Topics browse page (R008).
|
* Topics browse page (R008).
|
||||||
*
|
*
|
||||||
* Two-level hierarchy: 6 top-level categories with expandable/collapsible
|
* Responsive card grid layout with 7 top-level categories.
|
||||||
* sub-topics. Each sub-topic shows technique_count and creator_count.
|
* Each card shows: category name, description, summary stats
|
||||||
* Filter input narrows categories and sub-topics.
|
* (sub-topic count + total technique count), and an expand/collapse
|
||||||
* Click sub-topic → search results filtered to that topic.
|
* toggle revealing sub-topics as links to search.
|
||||||
|
*
|
||||||
|
* Filter input narrows visible cards by category name + sub-topic names.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { fetchTopics, type TopicCategory } from "../api/public-client";
|
import { fetchTopics, type TopicCategory } from "../api/public-client";
|
||||||
|
|
||||||
|
/** Derive the badge CSS slug from a category name. */
|
||||||
|
function catSlug(name: string): string {
|
||||||
|
return name.toLowerCase().replace(/\s+/g, "-");
|
||||||
|
}
|
||||||
|
|
||||||
export default function TopicsBrowse() {
|
export default function TopicsBrowse() {
|
||||||
const [categories, setCategories] = useState<TopicCategory[]>([]);
|
const [categories, setCategories] = useState<TopicCategory[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
@ -62,7 +69,7 @@ export default function TopicsBrowse() {
|
||||||
// Apply filter: show categories whose name or sub-topics match
|
// Apply filter: show categories whose name or sub-topics match
|
||||||
const lowerFilter = filter.toLowerCase();
|
const lowerFilter = filter.toLowerCase();
|
||||||
const filtered = filter
|
const filtered = filter
|
||||||
? categories
|
? (categories
|
||||||
.map((cat) => {
|
.map((cat) => {
|
||||||
const catMatches = cat.name.toLowerCase().includes(lowerFilter);
|
const catMatches = cat.name.toLowerCase().includes(lowerFilter);
|
||||||
const matchingSubs = cat.sub_topics.filter((st) =>
|
const matchingSubs = cat.sub_topics.filter((st) =>
|
||||||
|
|
@ -74,7 +81,7 @@ export default function TopicsBrowse() {
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
})
|
})
|
||||||
.filter(Boolean) as TopicCategory[]
|
.filter(Boolean) as TopicCategory[])
|
||||||
: categories;
|
: categories;
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
|
|
@ -104,28 +111,52 @@ export default function TopicsBrowse() {
|
||||||
|
|
||||||
{filtered.length === 0 ? (
|
{filtered.length === 0 ? (
|
||||||
<div className="empty-state">
|
<div className="empty-state">
|
||||||
No topics matching "{filter}"
|
No topics matching “{filter}”
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="topics-list">
|
<div className="topics-grid">
|
||||||
{filtered.map((cat) => (
|
{filtered.map((cat) => {
|
||||||
<div key={cat.name} className="topic-category">
|
const slug = catSlug(cat.name);
|
||||||
<button
|
const isExpanded = expanded.has(cat.name);
|
||||||
className="topic-category__header"
|
const totalTechniques = cat.sub_topics.reduce(
|
||||||
onClick={() => toggleCategory(cat.name)}
|
(sum, st) => sum + st.technique_count,
|
||||||
aria-expanded={expanded.has(cat.name)}
|
0,
|
||||||
>
|
);
|
||||||
<span className="topic-category__chevron">
|
|
||||||
{expanded.has(cat.name) ? "▼" : "▶"}
|
|
||||||
</span>
|
|
||||||
<span className="topic-category__name">{cat.name}</span>
|
|
||||||
<span className="topic-category__desc">{cat.description}</span>
|
|
||||||
<span className="topic-category__count">
|
|
||||||
{cat.sub_topics.length} sub-topic{cat.sub_topics.length !== 1 ? "s" : ""}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{expanded.has(cat.name) && (
|
return (
|
||||||
|
<div
|
||||||
|
key={cat.name}
|
||||||
|
className="topic-card"
|
||||||
|
style={{
|
||||||
|
borderLeftColor: `var(--color-badge-cat-${slug}-text)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="topic-card__body">
|
||||||
|
<h3 className="topic-card__name">
|
||||||
|
<span
|
||||||
|
className="topic-card__dot"
|
||||||
|
style={{
|
||||||
|
background: `var(--color-badge-cat-${slug}-text)`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{cat.name}
|
||||||
|
</h3>
|
||||||
|
<p className="topic-card__desc">{cat.description}</p>
|
||||||
|
<div className="topic-card__stats">
|
||||||
|
<span>{cat.sub_topics.length} sub-topic{cat.sub_topics.length !== 1 ? "s" : ""}</span>
|
||||||
|
<span className="topic-card__stats-sep">·</span>
|
||||||
|
<span>{totalTechniques} technique{totalTechniques !== 1 ? "s" : ""}</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="topic-card__toggle"
|
||||||
|
onClick={() => toggleCategory(cat.name)}
|
||||||
|
aria-expanded={isExpanded}
|
||||||
|
>
|
||||||
|
{isExpanded ? "Hide sub-topics ▲" : "Show sub-topics ▼"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isExpanded && (
|
||||||
<div className="topic-subtopics">
|
<div className="topic-subtopics">
|
||||||
{cat.sub_topics.map((st) => (
|
{cat.sub_topics.map((st) => (
|
||||||
<Link
|
<Link
|
||||||
|
|
@ -148,7 +179,8 @@ export default function TopicsBrowse() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue