feat: Moved Table of Contents from main prose column to sidebar top; re…
- "frontend/src/pages/TechniquePage.tsx" - "frontend/src/components/TableOfContents.tsx" - "frontend/src/App.css" GSD-Task: S04/T01
This commit is contained in:
parent
828afcc3e7
commit
1a7c11cac1
5 changed files with 54 additions and 30 deletions
24
alembic/versions/014_add_creator_avatar.py
Normal file
24
alembic/versions/014_add_creator_avatar.py
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
"""Add avatar columns to creators table.
|
||||
|
||||
Revision ID: 014_add_creator_avatar
|
||||
Revises: 013_add_search_log
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = "014_add_creator_avatar"
|
||||
down_revision = "013_add_search_log"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column("creators", sa.Column("avatar_url", sa.String(1000), nullable=True))
|
||||
op.add_column("creators", sa.Column("avatar_source", sa.String(50), nullable=True))
|
||||
op.add_column("creators", sa.Column("avatar_fetched_at", sa.TIMESTAMP(), nullable=True))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column("creators", "avatar_fetched_at")
|
||||
op.drop_column("creators", "avatar_source")
|
||||
op.drop_column("creators", "avatar_url")
|
||||
|
|
@ -103,6 +103,9 @@ class Creator(Base):
|
|||
slug: Mapped[str] = mapped_column(String(255), unique=True, nullable=False)
|
||||
genres: Mapped[list[str] | None] = mapped_column(ARRAY(String), nullable=True)
|
||||
folder_name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
avatar_url: Mapped[str | None] = mapped_column(String(1000), nullable=True)
|
||||
avatar_source: Mapped[str | None] = mapped_column(String(50), nullable=True)
|
||||
avatar_fetched_at: Mapped[datetime | None] = mapped_column(nullable=True)
|
||||
view_count: Mapped[int] = mapped_column(Integer, default=0, server_default="0")
|
||||
hidden: Mapped[bool] = mapped_column(default=False, server_default="false")
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
|
|
|
|||
|
|
@ -2032,10 +2032,8 @@ a.app-footer__repo:hover {
|
|||
/* ── Table of Contents ────────────────────────────────────────────────────── */
|
||||
|
||||
.technique-toc {
|
||||
background: var(--color-bg-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem 1.25rem;
|
||||
border-left: 2px solid var(--color-accent);
|
||||
padding: 0 0 0 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
|
|
@ -2052,56 +2050,52 @@ a.app-footer__repo:hover {
|
|||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
counter-reset: toc-section;
|
||||
}
|
||||
|
||||
.technique-toc__item {
|
||||
counter-increment: toc-section;
|
||||
margin-bottom: 0.25rem;
|
||||
margin-bottom: 0.125rem;
|
||||
}
|
||||
|
||||
.technique-toc__link {
|
||||
color: var(--color-accent);
|
||||
display: block;
|
||||
color: var(--color-text-secondary);
|
||||
text-decoration: none;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.technique-toc__link::before {
|
||||
content: counter(toc-section) ". ";
|
||||
color: var(--color-text-muted);
|
||||
padding: 0.125rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
transition: background 150ms ease, color 150ms ease;
|
||||
}
|
||||
|
||||
.technique-toc__link:hover {
|
||||
text-decoration: underline;
|
||||
background: var(--color-accent-subtle);
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.technique-toc__sublist {
|
||||
list-style: none;
|
||||
padding-left: 1.25rem;
|
||||
padding-left: 1rem;
|
||||
margin: 0.125rem 0 0.25rem;
|
||||
counter-reset: toc-sub;
|
||||
}
|
||||
|
||||
.technique-toc__subitem {
|
||||
counter-increment: toc-sub;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.technique-toc__sublink {
|
||||
color: var(--color-text-secondary);
|
||||
display: block;
|
||||
color: var(--color-text-muted);
|
||||
text-decoration: none;
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.technique-toc__sublink::before {
|
||||
content: counter(toc-section) "." counter(toc-sub) " ";
|
||||
color: var(--color-text-muted);
|
||||
padding: 0.125rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
transition: background 150ms ease, color 150ms ease;
|
||||
}
|
||||
|
||||
.technique-toc__sublink:hover {
|
||||
background: var(--color-accent-subtle);
|
||||
color: var(--color-accent);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* ── V2 subsections ───────────────────────────────────────────────────────── */
|
||||
|
|
|
|||
|
|
@ -23,8 +23,8 @@ export default function TableOfContents({ sections }: TableOfContentsProps) {
|
|||
|
||||
return (
|
||||
<nav className="technique-toc" aria-label="Table of contents">
|
||||
<h3 className="technique-toc__title">Contents</h3>
|
||||
<ol className="technique-toc__list">
|
||||
<h3 className="technique-toc__title">On this page</h3>
|
||||
<ul className="technique-toc__list">
|
||||
{sections.map((section) => {
|
||||
const sectionSlug = slugify(section.heading);
|
||||
return (
|
||||
|
|
@ -33,7 +33,7 @@ export default function TableOfContents({ sections }: TableOfContentsProps) {
|
|||
{section.heading}
|
||||
</a>
|
||||
{section.subsections.length > 0 && (
|
||||
<ol className="technique-toc__sublist">
|
||||
<ul className="technique-toc__sublist">
|
||||
{section.subsections.map((sub) => {
|
||||
const subSlug = `${sectionSlug}--${slugify(sub.heading)}`;
|
||||
return (
|
||||
|
|
@ -47,12 +47,12 @@ export default function TableOfContents({ sections }: TableOfContentsProps) {
|
|||
</li>
|
||||
);
|
||||
})}
|
||||
</ol>
|
||||
</ul>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ol>
|
||||
</ul>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -421,7 +421,6 @@ export default function TechniquePage() {
|
|||
<section className="technique-prose">
|
||||
{displayFormat === "v2" && Array.isArray(displaySections) ? (
|
||||
<>
|
||||
<TableOfContents sections={displaySections as BodySectionV2[]} />
|
||||
{(displaySections as BodySectionV2[]).map((section) => {
|
||||
const sectionSlug = slugify(section.heading);
|
||||
return (
|
||||
|
|
@ -469,6 +468,10 @@ export default function TechniquePage() {
|
|||
|
||||
</div>
|
||||
<div className="technique-columns__sidebar">
|
||||
{/* Table of Contents — v2 pages only */}
|
||||
{displayFormat === "v2" && Array.isArray(displaySections) && (displaySections as BodySectionV2[]).length > 0 && (
|
||||
<TableOfContents sections={displaySections as BodySectionV2[]} />
|
||||
)}
|
||||
{/* Key moments (always from live data — not versioned) */}
|
||||
{technique.key_moments.length > 0 && (
|
||||
<section className="technique-moments">
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue