Fix/css tweaks and bugs #11
@@ -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';
|
|
||||||
+5
-5
@@ -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: {
|
||||||
+10
-10
@@ -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
-2
@@ -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 @@
|
|||||||
export * from './SectionAccordion';
|
export * from './SectionPanel';
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export { SectionAccordion } from './ui/SectionAccordion/SectionAccordion';
|
||||||
+16
-16
@@ -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();
|
||||||
+4
-4
@@ -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 +0,0 @@
|
|||||||
export { SectionsAccordion } from './ui/SectionsAccordion/SectionsAccordion';
|
|
||||||
Reference in New Issue
Block a user