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:
jlightner 2026-04-03 05:52:47 +00:00
parent 828afcc3e7
commit 1a7c11cac1
5 changed files with 54 additions and 30 deletions

View 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")

View file

@ -103,6 +103,9 @@ class Creator(Base):
slug: Mapped[str] = mapped_column(String(255), unique=True, nullable=False) slug: Mapped[str] = mapped_column(String(255), unique=True, nullable=False)
genres: Mapped[list[str] | None] = mapped_column(ARRAY(String), nullable=True) genres: Mapped[list[str] | None] = mapped_column(ARRAY(String), nullable=True)
folder_name: Mapped[str] = mapped_column(String(255), nullable=False) 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") view_count: Mapped[int] = mapped_column(Integer, default=0, server_default="0")
hidden: Mapped[bool] = mapped_column(default=False, server_default="false") hidden: Mapped[bool] = mapped_column(default=False, server_default="false")
created_at: Mapped[datetime] = mapped_column( created_at: Mapped[datetime] = mapped_column(

View file

@ -2032,10 +2032,8 @@ a.app-footer__repo:hover {
/* ── Table of Contents ────────────────────────────────────────────────────── */ /* ── Table of Contents ────────────────────────────────────────────────────── */
.technique-toc { .technique-toc {
background: var(--color-bg-surface); border-left: 2px solid var(--color-accent);
border: 1px solid var(--color-border); padding: 0 0 0 1rem;
border-radius: 0.5rem;
padding: 1rem 1.25rem;
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
} }
@ -2052,56 +2050,52 @@ a.app-footer__repo:hover {
list-style: none; list-style: none;
padding: 0; padding: 0;
margin: 0; margin: 0;
counter-reset: toc-section;
} }
.technique-toc__item { .technique-toc__item {
counter-increment: toc-section; margin-bottom: 0.125rem;
margin-bottom: 0.25rem;
} }
.technique-toc__link { .technique-toc__link {
color: var(--color-accent); display: block;
color: var(--color-text-secondary);
text-decoration: none; text-decoration: none;
font-size: 0.875rem; font-size: 0.875rem;
line-height: 1.6; line-height: 1.6;
} padding: 0.125rem 0.5rem;
border-radius: 0.25rem;
.technique-toc__link::before { transition: background 150ms ease, color 150ms ease;
content: counter(toc-section) ". ";
color: var(--color-text-muted);
} }
.technique-toc__link:hover { .technique-toc__link:hover {
text-decoration: underline; background: var(--color-accent-subtle);
color: var(--color-accent);
} }
.technique-toc__sublist { .technique-toc__sublist {
list-style: none; list-style: none;
padding-left: 1.25rem; padding-left: 1rem;
margin: 0.125rem 0 0.25rem; margin: 0.125rem 0 0.25rem;
counter-reset: toc-sub;
} }
.technique-toc__subitem { .technique-toc__subitem {
counter-increment: toc-sub; margin-bottom: 0;
} }
.technique-toc__sublink { .technique-toc__sublink {
color: var(--color-text-secondary); display: block;
color: var(--color-text-muted);
text-decoration: none; text-decoration: none;
font-size: 0.8125rem; font-size: 0.8125rem;
line-height: 1.6; line-height: 1.6;
} padding: 0.125rem 0.5rem;
border-radius: 0.25rem;
.technique-toc__sublink::before { transition: background 150ms ease, color 150ms ease;
content: counter(toc-section) "." counter(toc-sub) " ";
color: var(--color-text-muted);
} }
.technique-toc__sublink:hover { .technique-toc__sublink:hover {
background: var(--color-accent-subtle);
color: var(--color-accent); color: var(--color-accent);
text-decoration: underline;
} }
/* ── V2 subsections ───────────────────────────────────────────────────────── */ /* ── V2 subsections ───────────────────────────────────────────────────────── */

View file

@ -23,8 +23,8 @@ export default function TableOfContents({ sections }: TableOfContentsProps) {
return ( return (
<nav className="technique-toc" aria-label="Table of contents"> <nav className="technique-toc" aria-label="Table of contents">
<h3 className="technique-toc__title">Contents</h3> <h3 className="technique-toc__title">On this page</h3>
<ol className="technique-toc__list"> <ul className="technique-toc__list">
{sections.map((section) => { {sections.map((section) => {
const sectionSlug = slugify(section.heading); const sectionSlug = slugify(section.heading);
return ( return (
@ -33,7 +33,7 @@ export default function TableOfContents({ sections }: TableOfContentsProps) {
{section.heading} {section.heading}
</a> </a>
{section.subsections.length > 0 && ( {section.subsections.length > 0 && (
<ol className="technique-toc__sublist"> <ul className="technique-toc__sublist">
{section.subsections.map((sub) => { {section.subsections.map((sub) => {
const subSlug = `${sectionSlug}--${slugify(sub.heading)}`; const subSlug = `${sectionSlug}--${slugify(sub.heading)}`;
return ( return (
@ -47,12 +47,12 @@ export default function TableOfContents({ sections }: TableOfContentsProps) {
</li> </li>
); );
})} })}
</ol> </ul>
)} )}
</li> </li>
); );
})} })}
</ol> </ul>
</nav> </nav>
); );
} }

View file

@ -421,7 +421,6 @@ export default function TechniquePage() {
<section className="technique-prose"> <section className="technique-prose">
{displayFormat === "v2" && Array.isArray(displaySections) ? ( {displayFormat === "v2" && Array.isArray(displaySections) ? (
<> <>
<TableOfContents sections={displaySections as BodySectionV2[]} />
{(displaySections as BodySectionV2[]).map((section) => { {(displaySections as BodySectionV2[]).map((section) => {
const sectionSlug = slugify(section.heading); const sectionSlug = slugify(section.heading);
return ( return (
@ -469,6 +468,10 @@ export default function TechniquePage() {
</div> </div>
<div className="technique-columns__sidebar"> <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) */} {/* Key moments (always from live data — not versioned) */}
{technique.key_moments.length > 0 && ( {technique.key_moments.length > 0 && (
<section className="technique-moments"> <section className="technique-moments">