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)
|
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(
|
||||||
|
|
|
||||||
|
|
@ -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 ───────────────────────────────────────────────────────── */
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue