fix: admin dropdown hover gap bridge + recent card footer layout (creator left, moments, date right)
This commit is contained in:
parent
b4bea10067
commit
e52506511d
4 changed files with 338 additions and 17 deletions
142
.planning/M016-ux-brand-reading-experience.md
Normal file
142
.planning/M016-ux-brand-reading-experience.md
Normal file
|
|
@ -0,0 +1,142 @@
|
||||||
|
# M016: UX Polish, Brand & Reading Experience
|
||||||
|
|
||||||
|
> **Stream:** Frontend — intended for a dedicated GSD milestone instance
|
||||||
|
> **Conflict zone:** `frontend/src/` only — no backend Python changes
|
||||||
|
> **Deploy cadence:** commit-build-redeploy after each slice completion
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Modernize the public site's visual identity and reading experience, fix pipeline admin UI bugs, and establish a brand baseline (logo, favicon, OG tags). Every change in this milestone lives entirely in the frontend — CSS, React components, and static assets.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Slice Breakdown
|
||||||
|
|
||||||
|
### S01: Landing Page Visual Fixes (Quick Wins)
|
||||||
|
**Risk:** Low | **Effort:** Small | **Files:** `App.css`, `Home.tsx`
|
||||||
|
|
||||||
|
Research found 5 concrete bugs/inconsistencies on the homepage:
|
||||||
|
|
||||||
|
| # | Issue | Fix |
|
||||||
|
|---|-------|-----|
|
||||||
|
| 1 | Duplicate `.btn` rule at App.css:3185 overrides CTA sizing (renders 131x38 instead of ~195x48) | Remove or merge the duplicate `.btn` block |
|
||||||
|
| 2 | `.home-featured` uses `border-image` which kills `border-radius` — card renders square | Replace with pseudo-element gradient border technique |
|
||||||
|
| 3 | Three different `max-width` tracks (36rem, 42rem, none) create jagged center column | Unify to 42rem for all content sections |
|
||||||
|
| 4 | Vertical spacing irregular — Random→Featured gap is only 8px vs 24px elsewhere | Normalize section margins to 1.5rem |
|
||||||
|
| 5 | Two `border-radius` values (0.5rem vs 0.625rem) on home cards | Unify to 0.625rem |
|
||||||
|
|
||||||
|
**Verification:** Visual screenshot comparison before/after on desktop (1280px) and mobile (375px).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### S02: Pipeline Admin UI Fixes
|
||||||
|
**Risk:** Low | **Effort:** Small-Medium | **Files:** `AdminPipeline.tsx`, `App.css`
|
||||||
|
|
||||||
|
Four issues identified with root causes already diagnosed:
|
||||||
|
|
||||||
|
| # | Issue | Root Cause | Fix |
|
||||||
|
|---|-------|-----------|-----|
|
||||||
|
| 1 | Most-recent run won't collapse / flickers | `expandedRunId` in `load()` useCallback dependency array (line 729) causes race condition — collapsing sets null, which triggers load recreation, which re-expands | Remove `expandedRunId` from dependency array + use `useRef` for initial-load tracking |
|
||||||
|
| 2 | Mobile job cards show vertical text ("C h e e") | `.pipeline-video__creator` missing overflow rules (App.css:4477) | Add `overflow: hidden; text-overflow: ellipsis; white-space: nowrap` (matches `.pipeline-video__filename` pattern) |
|
||||||
|
| 3 | No stage direction chevrons | Pipeline stages listed without visual flow indicator | Add CSS chevron/arrow between stage indicators using `::after` pseudo-elements or inline SVG |
|
||||||
|
| 4 | Filter text box should be replaced with button group | Current text input for status filter; should be "ALL \| Not Started \| In Progress \| Complete" buttons, end-aligned | Replace `<input>` with `<div className="filter-buttons">` flexbox, `justify-content: flex-end`, verify vertical alignment against adjacent elements |
|
||||||
|
|
||||||
|
**Verification:** Test collapse toggle on most-recent run, resize to 375px and check creator name truncation, confirm chevrons render between stages, confirm filter buttons align right and sit level with row.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### S03: Brand Minimum (Favicon, OG Tags, Logo)
|
||||||
|
**Risk:** Low | **Effort:** Small | **Files:** `index.html`, `App.tsx`, `App.css`, new static assets
|
||||||
|
|
||||||
|
The site currently has:
|
||||||
|
- No favicon (browser default icon)
|
||||||
|
- No OG meta tags (no preview image when sharing URL via text/Discord)
|
||||||
|
- No logo next to "Chrysopedia" in the header
|
||||||
|
|
||||||
|
Tasks:
|
||||||
|
1. **Design a simple logo** — something that fits the dark theme + cyan accent aesthetic. Could be a stylized book/page icon, a knowledge/crystal motif matching the "chryso-" (gold) prefix, or an abstract mark. Generate an SVG.
|
||||||
|
2. **Add favicon** — export logo as favicon.ico + apple-touch-icon + 192/512 PNG for PWA manifest
|
||||||
|
3. **Add OG meta tags** — `og:title`, `og:description`, `og:image`, `og:url`, `twitter:card` in index.html. Create a 1200x630 OG image using the logo + brand colors.
|
||||||
|
4. **Place logo in header** — render the SVG inline next to "Chrysopedia" text with appropriate sizing
|
||||||
|
|
||||||
|
**Verification:** Share URL in Discord/iMessage and confirm preview card renders. Check favicon in browser tab. Visual check logo in header.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### S04: ToC Modernization
|
||||||
|
**Risk:** Medium | **Effort:** Medium | **Files:** `TableOfContents.tsx`, `App.css`, `TechniquePage.tsx`
|
||||||
|
|
||||||
|
Research identified these dated elements:
|
||||||
|
- CSS counter numbering ("1.", "1.2") — biggest offender
|
||||||
|
- Boxed card container with solid border
|
||||||
|
- Uppercase "CONTENTS" label
|
||||||
|
- No active-section highlighting
|
||||||
|
- Underline-only hover states
|
||||||
|
|
||||||
|
Modernization plan:
|
||||||
|
1. **Remove numbered counters** — switch to unordered list with clean indentation
|
||||||
|
2. **Replace box border** with left accent bar (`border-left: 2px solid var(--color-accent)`)
|
||||||
|
3. **Change heading** from "CONTENTS" to "On this page" in sentence case
|
||||||
|
4. **Add hover background** (`rgba(34, 211, 238, 0.08)`) instead of underline
|
||||||
|
5. **Add IntersectionObserver** — track which section heading is in the viewport, highlight the corresponding ToC entry with accent left-border + brighter text color
|
||||||
|
6. **Make ToC sticky** — position it at the top of the existing sidebar (above Key Moments), `position: sticky; top: 1.5rem`
|
||||||
|
|
||||||
|
**Verification:** Navigate to a technique page with 4+ sections. Scroll through — ToC should highlight current section. ToC stays visible in sidebar while scrolling. Hover states work. No numbering visible.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### S05: Sticky Reading Header
|
||||||
|
**Risk:** Medium | **Effort:** Medium | **Files:** new `ReadingHeader.tsx`, `App.css`, `TechniquePage.tsx`
|
||||||
|
|
||||||
|
New component that slides in when user scrolls past the article title:
|
||||||
|
- Shows: article title (truncated) + current section name
|
||||||
|
- `position: sticky; top: 0; z-index: 50`
|
||||||
|
- Thin bar (~40px height), `var(--color-bg-header)` background, subtle bottom border
|
||||||
|
- Hidden by default, slides in via `transform: translateY(-100%)` → `translateY(0)` transition
|
||||||
|
- Uses IntersectionObserver on the technique header element as show/hide trigger
|
||||||
|
- Shares the section-tracking observer from S04's ToC work
|
||||||
|
- On mobile: compact single-line with optional dropdown for section jump
|
||||||
|
- Update `scroll-margin-top` values on section anchors to account for new header height
|
||||||
|
|
||||||
|
**Verification:** Open long technique page. Scroll past title — reading header appears. Correct section name updates as you scroll. Works on mobile (375px). Doesn't break existing header.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### S06: Landing Page Personality Pass
|
||||||
|
**Risk:** Low | **Effort:** Small | **Files:** `Home.tsx`, `App.css`
|
||||||
|
|
||||||
|
After S01 fixes the bugs, this slice adds polish:
|
||||||
|
1. **Hero tightening** — reduce hero bottom padding and how-it-works top margin to get content above the fold faster (~40-50px reclaim)
|
||||||
|
2. **Stats scorecard enhancement** — animated count-up on first view (simple `requestAnimationFrame` counter), subtle glow on numbers
|
||||||
|
3. **Random button treatment** — wrap in a small card with "Feeling adventurous?" tagline, or embed as secondary action inside Trending Searches
|
||||||
|
4. **Section heading standardization** — pick one treatment (title-case with left accent bar) and apply consistently to "Recently Added", "Trending Searches", "Popular Topics"
|
||||||
|
5. **Header brand accent** — apply `color: var(--color-accent)` or subtle gradient to "Chrysopedia" text (pairs with S03 logo)
|
||||||
|
|
||||||
|
**Verification:** Visual check desktop + mobile. Stats animate on page load. Section headings consistent. Content peeks above fold on standard viewport.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dependency Graph
|
||||||
|
|
||||||
|
```
|
||||||
|
S01 (landing fixes) ──→ S06 (personality pass)
|
||||||
|
S02 (pipeline fixes) [independent]
|
||||||
|
S03 (brand minimum) ──→ S06 (header accent uses logo)
|
||||||
|
S04 (ToC modern) ──→ S05 (reading header shares IntersectionObserver pattern)
|
||||||
|
```
|
||||||
|
|
||||||
|
S01, S02, S03, S04 can all start in parallel. S05 depends on S04. S06 depends on S01 + S03.
|
||||||
|
|
||||||
|
Recommended execution order: **S01 → S02 → S03 → S04 → S05 → S06**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Out of Scope (for this milestone)
|
||||||
|
|
||||||
|
- Creator landing page redesign (depends on backend social links API — see M017)
|
||||||
|
- Auto-avatar images (backend-gated — see M017)
|
||||||
|
- Embed tab (needs backend investigation first — see M017)
|
||||||
|
- Any backend Python changes
|
||||||
|
- M015 S04/S05 leftovers (trending searches block, admin dropdown hover) — should be completed by M015's own GSD session first
|
||||||
164
.planning/M017-backend-perf-creator-features.md
Normal file
164
.planning/M017-backend-perf-creator-features.md
Normal file
|
|
@ -0,0 +1,164 @@
|
||||||
|
# M017: Backend Performance & Creator Features
|
||||||
|
|
||||||
|
> **Stream:** Backend — intended for a separate Claude Code session (not GSD)
|
||||||
|
> **Conflict zone:** `backend/` only + one frontend API client fix
|
||||||
|
> **Deploy cadence:** commit-build-redeploy after each task group
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Fix critical performance bottlenecks in the admin pipeline API, implement auto-avatar fetching for creators, and lay the backend groundwork for creator landing page improvements. Almost entirely backend Python — minimal frontend changes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task Groups (execute sequentially)
|
||||||
|
|
||||||
|
### 1. Critical Fixes (do first, immediate impact)
|
||||||
|
|
||||||
|
#### 1a. Fix `worker-status` async event loop blocking
|
||||||
|
**File:** `backend/routers/pipeline.py` lines 1266-1313
|
||||||
|
**Problem:** The endpoint is `async def` but calls three synchronous Celery inspect methods (`inspector.active()`, `.reserved()`, `.stats()`), each with a 1-second timeout. This blocks the entire uvicorn event loop for ~3 seconds, stalling ALL concurrent API requests.
|
||||||
|
**Evidence:** Every parallel API call during page load takes ~3,024ms instead of their natural 6-38ms.
|
||||||
|
**Fix:**
|
||||||
|
```python
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
active = await asyncio.to_thread(inspector.active) or {}
|
||||||
|
reserved = await asyncio.to_thread(inspector.reserved) or {}
|
||||||
|
stats = await asyncio.to_thread(inspector.stats) or {}
|
||||||
|
```
|
||||||
|
Also consider: reduce inspect timeout to 0.5s, add Redis cache with 10-15s TTL to avoid repeated slow calls.
|
||||||
|
**Impact:** Page load drops from ~3s to ~50ms.
|
||||||
|
|
||||||
|
#### 1b. Fix creators endpoint 422 error
|
||||||
|
**File (frontend):** `frontend/src/pages/AdminPipeline.tsx` line 1126
|
||||||
|
**File (backend):** `backend/routers/creators.py` line 28
|
||||||
|
**Problem:** Frontend requests `fetchCreators({ limit: 200 })` but backend validates `le=100`. Returns 422 on every pipeline page load — creator filter dropdown never populates.
|
||||||
|
**Fix:** Change frontend call to `limit: 100`. (This is the ONE frontend file touch in this stream.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Pipeline API Performance
|
||||||
|
|
||||||
|
#### 2a. Rewrite `stale-pages` to eliminate N+1 queries
|
||||||
|
**File:** `backend/routers/pipeline.py` lines 906-973
|
||||||
|
**Problem:** Loads ALL technique pages, then runs a separate query per page for latest version + another for creator name. Currently 44 extra queries for 22 pages. Fast today (~30ms) but degrades linearly.
|
||||||
|
**Fix:** Single query joining `technique_pages` with a lateral/window subquery for latest version + join to creators.
|
||||||
|
|
||||||
|
#### 2b. Add pagination to videos endpoint
|
||||||
|
**File:** `backend/routers/pipeline.py` lines 72-188
|
||||||
|
**Problem:** Returns all 43 videos (23KB) with no offset/limit. Client-side filtering only.
|
||||||
|
**Fix:** Add `offset`, `limit`, `status`, `creator_id` query params. Return paginated response with `total` count. Frontend can adopt server-side filtering later (or the M016 frontend stream can wire it up).
|
||||||
|
|
||||||
|
#### 2c. Optimize `_find_dynamic_related` for technique pages
|
||||||
|
**File:** `backend/routers/techniques.py` lines 33-111
|
||||||
|
**Problem:** Loads ALL technique pages into memory to score relatedness in Python. O(n) in total page count.
|
||||||
|
**Fix:** Move scoring to SQL (keyword overlap via `ts_rank` or simple tag intersection) or cache related links per technique page with invalidation on new page creation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Auto-Avatar Integration (TheAudioDB)
|
||||||
|
|
||||||
|
Research concluded: **TheAudioDB is the best first source** — free, no OAuth, no caching restrictions, decent coverage for established artists.
|
||||||
|
|
||||||
|
#### 3a. Database migration
|
||||||
|
Add to `Creator` model:
|
||||||
|
- `avatar_url: String | None` — stored image URL or local path
|
||||||
|
- `avatar_source: String` — enum: `"generated"`, `"theaudiodb"`, `"manual"`
|
||||||
|
- `avatar_fetched_at: DateTime | None` — for cache invalidation
|
||||||
|
|
||||||
|
Alembic migration (will be 014 or later depending on M015 state).
|
||||||
|
|
||||||
|
#### 3b. TheAudioDB lookup service
|
||||||
|
New file: `backend/services/avatar.py`
|
||||||
|
- `async def fetch_avatar(creator_name: str, creator_genres: list[str]) -> AvatarResult | None`
|
||||||
|
- Calls `https://www.theaudiodb.com/api/v1/json/{key}/search.php?s={name}`
|
||||||
|
- Confidence scoring: name match via `thefuzz.fuzz.token_sort_ratio` (threshold ≥ 85%), genre overlap as tiebreaker
|
||||||
|
- Returns `strArtistThumb` URL if confident match, None otherwise
|
||||||
|
- Handle: no results, multiple results, missing image fields
|
||||||
|
|
||||||
|
#### 3c. Celery worker task
|
||||||
|
New task: `tasks.fetch_creator_avatar`
|
||||||
|
- Called on creator creation or manually via admin endpoint
|
||||||
|
- Runs TheAudioDB lookup → downloads image → stores locally (or stores URL)
|
||||||
|
- Updates `Creator.avatar_url`, `avatar_source`, `avatar_fetched_at`
|
||||||
|
- Falls back gracefully — if no match, leaves fields null (frontend already renders generated SVG as fallback)
|
||||||
|
|
||||||
|
#### 3d. Admin endpoint for manual trigger
|
||||||
|
`POST /admin/pipeline/creators/{id}/fetch-avatar` — triggers the worker task for a specific creator.
|
||||||
|
`POST /admin/pipeline/creators/fetch-all-avatars` — batch trigger for all creators missing avatars.
|
||||||
|
|
||||||
|
#### 3e. Wire avatar_url into creators API responses
|
||||||
|
Add `avatar_url` to `CreatorBrowseItem` and `CreatorDetail` schemas. The frontend `CreatorAvatar` component already accepts an `imageUrl` prop — it will just work once the API returns it.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Creator Landing Page API Groundwork
|
||||||
|
|
||||||
|
#### 4a. Social links model + migration
|
||||||
|
Add to `Creator` model:
|
||||||
|
- `social_links: JSON | None` — structured as `{"spotify": "url", "instagram": "url", "bandcamp": "url", "website": "url", ...}`
|
||||||
|
- `bio: Text | None` — short creator bio/description
|
||||||
|
- `featured: Boolean` — flag for homepage featuring
|
||||||
|
|
||||||
|
#### 4b. Creator detail endpoint enhancement
|
||||||
|
Expand `GET /api/v1/creators/{slug}` to return:
|
||||||
|
- `social_links`
|
||||||
|
- `bio`
|
||||||
|
- `avatar_url`
|
||||||
|
- `technique_count`
|
||||||
|
- Full technique list with titles, slugs, created_at
|
||||||
|
- Genre breakdown
|
||||||
|
|
||||||
|
#### 4c. Admin endpoint for creator profile editing
|
||||||
|
`PUT /admin/pipeline/creators/{id}` — update `bio`, `social_links`, `featured` flag, manually set `avatar_url`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. Embed Tab Investigation
|
||||||
|
The "Embed" tab under pipeline jobs is non-functional. Before building, need to:
|
||||||
|
- Read the existing frontend component to understand what it expects
|
||||||
|
- Determine what "Embed" should show (embedding vectors? embed codes? embedded content?)
|
||||||
|
- If it's about Qdrant vector embeddings: add an endpoint to query embedding status per technique page
|
||||||
|
- If it's about iframe embed codes: generate shareable snippet per technique
|
||||||
|
|
||||||
|
**This task starts as investigation — scope will be defined after reading the code.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## General Load Time Optimization (apply throughout)
|
||||||
|
|
||||||
|
As each endpoint is touched, also consider:
|
||||||
|
- Add `Cache-Control` headers for public GET endpoints (technique pages, creators, search suggestions)
|
||||||
|
- Add Redis caching (30s-5min TTL) for expensive or frequently-hit endpoints
|
||||||
|
- Ensure database indexes exist on commonly filtered/sorted columns
|
||||||
|
- Consider adding `select_in_loading` for SQLAlchemy relationships to avoid implicit lazy loads
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Files
|
||||||
|
|
||||||
|
| File | What changes |
|
||||||
|
|------|-------------|
|
||||||
|
| `backend/routers/pipeline.py` | worker-status async fix, stale-pages rewrite, videos pagination, avatar endpoints |
|
||||||
|
| `backend/routers/creators.py` | Creator detail expansion, social links |
|
||||||
|
| `backend/routers/techniques.py` | Related techniques optimization |
|
||||||
|
| `backend/models.py` | Creator model additions (avatar, social_links, bio) |
|
||||||
|
| `backend/schemas.py` | New response schemas |
|
||||||
|
| `backend/services/avatar.py` | New — TheAudioDB integration |
|
||||||
|
| `backend/tasks.py` | New avatar fetch task |
|
||||||
|
| `alembic/versions/014_*.py` | Migration for creator columns |
|
||||||
|
| `frontend/src/pages/AdminPipeline.tsx` | Line 1126 only — fix limit: 200 → 100 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Merge Coordination with M016
|
||||||
|
|
||||||
|
These two streams are designed to have minimal file overlap:
|
||||||
|
- **M016 touches:** `App.css`, `Home.tsx`, `TechniquePage.tsx`, `TableOfContents.tsx`, `AdminPipeline.tsx` (CSS/JSX only), new frontend components, static assets
|
||||||
|
- **M017 touches:** `backend/` (routers, models, schemas, services, tasks), `alembic/`, one line in `AdminPipeline.tsx`
|
||||||
|
|
||||||
|
The single conflict point is `AdminPipeline.tsx` — M017's creators limit fix (line 1126) vs M016's pipeline UI fixes (collapse bug at line 729, filter buttons, chevrons). Resolve by merging M017's one-liner first, then M016's broader changes on top.
|
||||||
|
|
||||||
|
For the avatar/social-links frontend wiring: M017 ships the API, M016 (or a follow-up) consumes it. No conflict — just sequencing.
|
||||||
|
|
@ -1028,6 +1028,16 @@ a.app-footer__repo:hover {
|
||||||
padding: 0.375rem 0;
|
padding: 0.375rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Bridge the gap between trigger and menu so hover doesn't break */
|
||||||
|
.admin-dropdown__menu::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -0.5rem;
|
||||||
|
right: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.admin-dropdown__item {
|
.admin-dropdown__item {
|
||||||
display: block;
|
display: block;
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
|
|
@ -5097,13 +5107,9 @@ a.app-footer__repo:hover {
|
||||||
}
|
}
|
||||||
|
|
||||||
.recent-card__creator {
|
.recent-card__creator {
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.3rem;
|
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
color: var(--color-text-secondary);
|
color: var(--color-text-secondary);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.recent-card__header-right {
|
.recent-card__header-right {
|
||||||
|
|
@ -5114,11 +5120,22 @@ a.app-footer__repo:hover {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.recent-card__footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
padding-top: 0.375rem;
|
||||||
|
border-top: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
.recent-card__date {
|
.recent-card__date {
|
||||||
font-size: 0.7rem;
|
font-size: 0.7rem;
|
||||||
color: var(--color-text-secondary);
|
color: var(--color-text-secondary);
|
||||||
opacity: 0.65;
|
opacity: 0.65;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Search result card creator ───────────────────────────────────────── */
|
/* ── Search result card creator ───────────────────────────────────────── */
|
||||||
|
|
|
||||||
|
|
@ -310,19 +310,7 @@ export default function Home() {
|
||||||
className="recent-card card-stagger"
|
className="recent-card card-stagger"
|
||||||
style={{ '--stagger-index': i } as React.CSSProperties}
|
style={{ '--stagger-index': i } as React.CSSProperties}
|
||||||
>
|
>
|
||||||
<span className="recent-card__header">
|
<span className="recent-card__title">{t.title}</span>
|
||||||
<span className="recent-card__title">{t.title}</span>
|
|
||||||
<span className="recent-card__header-right">
|
|
||||||
{t.creator_name && (
|
|
||||||
<span className="recent-card__creator">{t.creator_name}</span>
|
|
||||||
)}
|
|
||||||
{t.created_at && (
|
|
||||||
<span className="recent-card__date">
|
|
||||||
{new Date(t.created_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<span className="recent-card__meta">
|
<span className="recent-card__meta">
|
||||||
<span className="badge badge--category">
|
<span className="badge badge--category">
|
||||||
{t.topic_category}
|
{t.topic_category}
|
||||||
|
|
@ -337,11 +325,21 @@ export default function Home() {
|
||||||
: t.summary}
|
: t.summary}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
</span>
|
||||||
|
<span className="recent-card__footer">
|
||||||
|
<span className="recent-card__creator">
|
||||||
|
{t.creator_name || ''}
|
||||||
|
</span>
|
||||||
{t.key_moment_count > 0 && (
|
{t.key_moment_count > 0 && (
|
||||||
<span className="recent-card__moments">
|
<span className="recent-card__moments">
|
||||||
{t.key_moment_count} moment{t.key_moment_count !== 1 ? 's' : ''}
|
{t.key_moment_count} moment{t.key_moment_count !== 1 ? 's' : ''}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{t.created_at && (
|
||||||
|
<span className="recent-card__date">
|
||||||
|
{new Date(t.created_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue