Fix/css tweaks and bugs #11

Merged
ilia merged 13 commits from fix/css-tweaks-and-bugs into main 2026-07-14 15:19:16 +00:00
11 changed files with 43 additions and 43 deletions
Showing only changes of commit 702be45ce3 - Show all commits
+3 -3
View File
@@ -1,8 +1,8 @@
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
import type { SectionRecord } from '$entities/Section'; import type { SectionRecord } from '$entities/Section';
import { getCollection } from '$shared/api'; import { getCollection } from '$shared/api';
import { SectionAccordion } from '$widgets/SectionAccordion';
import { SectionFactory } from '$widgets/SectionFactory'; import { SectionFactory } from '$widgets/SectionFactory';
import { SectionsAccordion } from '$widgets/SectionsAccordion';
/** /**
* Optional catchall: `/` → first section, `/:slug` → that section. * Optional catchall: `/` → first section, `/:slug` → that section.
@@ -46,11 +46,11 @@ export default async function SectionPage({ params }: Props) {
return ( return (
<main className="px-4 py-6 sm:px-8 sm:py-12 lg:py-16 lg:px-16"> <main className="px-4 py-6 sm:px-8 sm:py-12 lg:py-16 lg:px-16">
<SectionsAccordion sections={sections} activeSlug={activeSlug}> <SectionAccordion sections={sections} activeSlug={activeSlug}>
{sections.map((s) => ( {sections.map((s) => (
<SectionFactory key={s.slug} slug={s.slug} /> <SectionFactory key={s.slug} slug={s.slug} />
))} ))}
</SectionsAccordion> </SectionAccordion>
</main> </main>
); );
} }
@@ -1 +0,0 @@
export * from './SectionAccordion';
@@ -1,9 +1,9 @@
import type { Meta, StoryObj } from '@storybook/nextjs-vite'; import type { Meta, StoryObj } from '@storybook/nextjs-vite';
import { SectionAccordion } from './SectionAccordion'; import { SectionPanel } from './SectionPanel';
const meta: Meta<typeof SectionAccordion> = { const meta: Meta<typeof SectionPanel> = {
title: 'Shared/SectionAccordion', title: 'Shared/SectionPanel',
component: SectionAccordion, component: SectionPanel,
decorators: [ decorators: [
(Story) => ( (Story) => (
<div className="p-8 bg-ochre-clay"> <div className="p-8 bg-ochre-clay">
@@ -15,7 +15,7 @@ const meta: Meta<typeof SectionAccordion> = {
export default meta; export default meta;
type Story = StoryObj<typeof SectionAccordion>; type Story = StoryObj<typeof SectionPanel>;
export const Active: Story = { export const Active: Story = {
args: { args: {
@@ -1,5 +1,5 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { SectionAccordion } from './SectionAccordion'; import { SectionPanel } from './SectionPanel';
const defaultProps = { const defaultProps = {
number: '01', number: '01',
@@ -10,30 +10,30 @@ const defaultProps = {
children: <p>Content here</p>, children: <p>Content here</p>,
}; };
describe('SectionAccordion', () => { describe('SectionPanel', () => {
describe('collapsed state (isActive=false)', () => { describe('collapsed state (isActive=false)', () => {
it('renders a section element with the given id', () => { it('renders a section element with the given id', () => {
const { container } = render(<SectionAccordion {...defaultProps} />); const { container } = render(<SectionPanel {...defaultProps} />);
expect(container.querySelector('section#about')).toBeInTheDocument(); expect(container.querySelector('section#about')).toBeInTheDocument();
}); });
it('renders a link with number and title', () => { it('renders a link with number and title', () => {
render(<SectionAccordion {...defaultProps} />); render(<SectionPanel {...defaultProps} />);
expect(screen.getByRole('link', { name: /01.*About/i })).toBeInTheDocument(); expect(screen.getByRole('link', { name: /01.*About/i })).toBeInTheDocument();
}); });
it('link points to the correct href', () => { it('link points to the correct href', () => {
render(<SectionAccordion {...defaultProps} />); render(<SectionPanel {...defaultProps} />);
expect(screen.getByRole('link', { name: /01.*About/i })).toHaveAttribute('href', '/about'); expect(screen.getByRole('link', { name: /01.*About/i })).toHaveAttribute('href', '/about');
}); });
it('does not render children', () => { it('does not render children', () => {
render(<SectionAccordion {...defaultProps} />); render(<SectionPanel {...defaultProps} />);
expect(screen.queryByText('Content here')).not.toBeInTheDocument(); expect(screen.queryByText('Content here')).not.toBeInTheDocument();
}); });
it('does not render a button', () => { it('does not render a button', () => {
render(<SectionAccordion {...defaultProps} />); render(<SectionPanel {...defaultProps} />);
expect(screen.queryByRole('button')).not.toBeInTheDocument(); expect(screen.queryByRole('button')).not.toBeInTheDocument();
}); });
}); });
@@ -42,17 +42,17 @@ describe('SectionAccordion', () => {
const activeProps = { ...defaultProps, isActive: true }; const activeProps = { ...defaultProps, isActive: true };
it('renders an h1 with number and title', () => { it('renders an h1 with number and title', () => {
render(<SectionAccordion {...activeProps} />); render(<SectionPanel {...activeProps} />);
expect(screen.getByRole('heading', { level: 1, name: /01.*About/i })).toBeInTheDocument(); expect(screen.getByRole('heading', { level: 1, name: /01.*About/i })).toBeInTheDocument();
}); });
it('renders children', () => { it('renders children', () => {
render(<SectionAccordion {...activeProps} />); render(<SectionPanel {...activeProps} />);
expect(screen.getByText('Content here')).toBeInTheDocument(); expect(screen.getByText('Content here')).toBeInTheDocument();
}); });
it('does not render a link', () => { it('does not render a link', () => {
render(<SectionAccordion {...activeProps} />); render(<SectionPanel {...activeProps} />);
expect(screen.queryByRole('link')).not.toBeInTheDocument(); expect(screen.queryByRole('link')).not.toBeInTheDocument();
}); });
}); });
@@ -2,7 +2,7 @@ import Link from 'next/link';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { ViewTransitionWrapper } from '$shared/ui'; import { ViewTransitionWrapper } from '$shared/ui';
interface SectionAccordionProps { interface SectionPanelProps {
/** /**
* Display number prefix (e.g. "01") * Display number prefix (e.g. "01")
*/ */
@@ -32,7 +32,7 @@ interface SectionAccordionProps {
/** /**
* Accordion-style section that collapses to a navigation link when inactive. * Accordion-style section that collapses to a navigation link when inactive.
*/ */
export function SectionAccordion({ number, title, id, isActive, href, children }: SectionAccordionProps) { export function SectionPanel({ number, title, id, isActive, href, children }: SectionPanelProps) {
const heading = `${number}. ${title}`; const heading = `${number}. ${title}`;
return ( return (
@@ -0,0 +1 @@
export * from './SectionPanel';
+1 -1
View File
@@ -1 +1 @@
export * from './SectionAccordion'; export * from './SectionPanel';
+1
View File
@@ -0,0 +1 @@
export { SectionAccordion } from './ui/SectionAccordion/SectionAccordion';
@@ -1,6 +1,6 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import type { SectionRecord } from '$entities/Section'; import type { SectionRecord } from '$entities/Section';
import { SectionsAccordion } from './SectionsAccordion'; import { SectionAccordion } from './SectionAccordion';
const baseRecord = { collectionId: 'c1', collectionName: 'sections', created: '', updated: '' }; const baseRecord = { collectionId: 'c1', collectionName: 'sections', created: '', updated: '' };
@@ -10,26 +10,26 @@ const sections: SectionRecord[] = [
{ ...baseRecord, id: '3', slug: 'skills', title: 'Skills', order: 3 }, { ...baseRecord, id: '3', slug: 'skills', title: 'Skills', order: 3 },
]; ];
describe('SectionsAccordion', () => { describe('SectionAccordion', () => {
describe('active section rendering', () => { describe('active section rendering', () => {
it('renders the active section as an h1', () => { it('renders the active section as an h1', () => {
render( render(
<SectionsAccordion sections={sections} activeSlug="intro"> <SectionAccordion sections={sections} activeSlug="intro">
<div>Intro content</div> <div>Intro content</div>
<div>Bio content</div> <div>Bio content</div>
<div>Skills content</div> <div>Skills content</div>
</SectionsAccordion>, </SectionAccordion>,
); );
expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent('01. Intro'); expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent('01. Intro');
}); });
it('renders inactive sections as links', () => { it('renders inactive sections as links', () => {
render( render(
<SectionsAccordion sections={sections} activeSlug="intro"> <SectionAccordion sections={sections} activeSlug="intro">
<div>Intro content</div> <div>Intro content</div>
<div>Bio content</div> <div>Bio content</div>
<div>Skills content</div> <div>Skills content</div>
</SectionsAccordion>, </SectionAccordion>,
); );
const links = screen.getAllByRole('link'); const links = screen.getAllByRole('link');
expect(links).toHaveLength(2); expect(links).toHaveLength(2);
@@ -37,11 +37,11 @@ describe('SectionsAccordion', () => {
it('inactive section links point to correct hrefs', () => { it('inactive section links point to correct hrefs', () => {
render( render(
<SectionsAccordion sections={sections} activeSlug="intro"> <SectionAccordion sections={sections} activeSlug="intro">
<div>Intro content</div> <div>Intro content</div>
<div>Bio content</div> <div>Bio content</div>
<div>Skills content</div> <div>Skills content</div>
</SectionsAccordion>, </SectionAccordion>,
); );
expect(screen.getByRole('link', { name: /02.*Bio/i })).toHaveAttribute('href', '/bio'); expect(screen.getByRole('link', { name: /02.*Bio/i })).toHaveAttribute('href', '/bio');
expect(screen.getByRole('link', { name: /03.*Skills/i })).toHaveAttribute('href', '/skills'); expect(screen.getByRole('link', { name: /03.*Skills/i })).toHaveAttribute('href', '/skills');
@@ -49,22 +49,22 @@ describe('SectionsAccordion', () => {
it('renders the correct active section for a given activeSlug', () => { it('renders the correct active section for a given activeSlug', () => {
render( render(
<SectionsAccordion sections={sections} activeSlug="bio"> <SectionAccordion sections={sections} activeSlug="bio">
<div>Intro content</div> <div>Intro content</div>
<div>Bio content</div> <div>Bio content</div>
<div>Skills content</div> <div>Skills content</div>
</SectionsAccordion>, </SectionAccordion>,
); );
expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent('02. Bio'); expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent('02. Bio');
}); });
it('only one section is active at a time', () => { it('only one section is active at a time', () => {
render( render(
<SectionsAccordion sections={sections} activeSlug="skills"> <SectionAccordion sections={sections} activeSlug="skills">
<div>Intro content</div> <div>Intro content</div>
<div>Bio content</div> <div>Bio content</div>
<div>Skills content</div> <div>Skills content</div>
</SectionsAccordion>, </SectionAccordion>,
); );
expect(screen.getAllByRole('heading', { level: 1 })).toHaveLength(1); expect(screen.getAllByRole('heading', { level: 1 })).toHaveLength(1);
}); });
@@ -73,22 +73,22 @@ describe('SectionsAccordion', () => {
describe('content slots', () => { describe('content slots', () => {
it('shows active section content', () => { it('shows active section content', () => {
render( render(
<SectionsAccordion sections={sections} activeSlug="intro"> <SectionAccordion sections={sections} activeSlug="intro">
<div>Intro content</div> <div>Intro content</div>
<div>Bio content</div> <div>Bio content</div>
<div>Skills content</div> <div>Skills content</div>
</SectionsAccordion>, </SectionAccordion>,
); );
expect(screen.getByText('Intro content')).toBeInTheDocument(); expect(screen.getByText('Intro content')).toBeInTheDocument();
}); });
it('does not show inactive section content', () => { it('does not show inactive section content', () => {
render( render(
<SectionsAccordion sections={sections} activeSlug="intro"> <SectionAccordion sections={sections} activeSlug="intro">
<div>Intro content</div> <div>Intro content</div>
<div>Bio content</div> <div>Bio content</div>
<div>Skills content</div> <div>Skills content</div>
</SectionsAccordion>, </SectionAccordion>,
); );
expect(screen.queryByText('Bio content')).not.toBeInTheDocument(); expect(screen.queryByText('Bio content')).not.toBeInTheDocument();
expect(screen.queryByText('Skills content')).not.toBeInTheDocument(); expect(screen.queryByText('Skills content')).not.toBeInTheDocument();
@@ -1,7 +1,7 @@
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { Children } from 'react'; import { Children } from 'react';
import type { SectionRecord } from '$entities/Section'; import type { SectionRecord } from '$entities/Section';
import { SectionAccordion } from '$entities/Section'; import { SectionPanel } from '$entities/Section';
export interface Props { export interface Props {
/** /**
@@ -24,13 +24,13 @@ export interface Props {
* Active section is determined by the URL (activeSlug prop); inactive sections * Active section is determined by the URL (activeSlug prop); inactive sections
* render as navigation links so the browser handles routing. * render as navigation links so the browser handles routing.
*/ */
export function SectionsAccordion({ sections, activeSlug, children }: Props) { export function SectionAccordion({ sections, activeSlug, children }: Props) {
const slots = Children.toArray(children); const slots = Children.toArray(children);
return ( return (
<div> <div>
{sections.map((section, i) => ( {sections.map((section, i) => (
<SectionAccordion <SectionPanel
key={section.slug} key={section.slug}
id={section.slug} id={section.slug}
number={String(section.order).padStart(2, '0')} number={String(section.order).padStart(2, '0')}
@@ -39,7 +39,7 @@ export function SectionsAccordion({ sections, activeSlug, children }: Props) {
href={`/${section.slug}`} href={`/${section.slug}`}
> >
{slots[i]} {slots[i]}
</SectionAccordion> </SectionPanel>
))} ))}
</div> </div>
); );
-1
View File
@@ -1 +0,0 @@
export { SectionsAccordion } from './ui/SectionsAccordion/SectionsAccordion';