Fix/css tweaks and bugs #11
@@ -1,8 +1,8 @@
|
||||
import { notFound } from 'next/navigation';
|
||||
import type { SectionRecord } from '$entities/Section';
|
||||
import { getCollection } from '$shared/api';
|
||||
import { SectionAccordion } from '$widgets/SectionAccordion';
|
||||
import { SectionFactory } from '$widgets/SectionFactory';
|
||||
import { SectionsAccordion } from '$widgets/SectionsAccordion';
|
||||
|
||||
/**
|
||||
* Optional catchall: `/` → first section, `/:slug` → that section.
|
||||
@@ -46,11 +46,11 @@ export default async function SectionPage({ params }: Props) {
|
||||
|
||||
return (
|
||||
<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) => (
|
||||
<SectionFactory key={s.slug} slug={s.slug} />
|
||||
))}
|
||||
</SectionsAccordion>
|
||||
</SectionAccordion>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export * from './SectionAccordion';
|
||||
+5
-5
@@ -1,9 +1,9 @@
|
||||
import type { Meta, StoryObj } from '@storybook/nextjs-vite';
|
||||
import { SectionAccordion } from './SectionAccordion';
|
||||
import { SectionPanel } from './SectionPanel';
|
||||
|
||||
const meta: Meta<typeof SectionAccordion> = {
|
||||
title: 'Shared/SectionAccordion',
|
||||
component: SectionAccordion,
|
||||
const meta: Meta<typeof SectionPanel> = {
|
||||
title: 'Shared/SectionPanel',
|
||||
component: SectionPanel,
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="p-8 bg-ochre-clay">
|
||||
@@ -15,7 +15,7 @@ const meta: Meta<typeof SectionAccordion> = {
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof SectionAccordion>;
|
||||
type Story = StoryObj<typeof SectionPanel>;
|
||||
|
||||
export const Active: Story = {
|
||||
args: {
|
||||
+10
-10
@@ -1,5 +1,5 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { SectionAccordion } from './SectionAccordion';
|
||||
import { SectionPanel } from './SectionPanel';
|
||||
|
||||
const defaultProps = {
|
||||
number: '01',
|
||||
@@ -10,30 +10,30 @@ const defaultProps = {
|
||||
children: <p>Content here</p>,
|
||||
};
|
||||
|
||||
describe('SectionAccordion', () => {
|
||||
describe('SectionPanel', () => {
|
||||
describe('collapsed state (isActive=false)', () => {
|
||||
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();
|
||||
});
|
||||
|
||||
it('renders a link with number and title', () => {
|
||||
render(<SectionAccordion {...defaultProps} />);
|
||||
render(<SectionPanel {...defaultProps} />);
|
||||
expect(screen.getByRole('link', { name: /01.*About/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('link points to the correct href', () => {
|
||||
render(<SectionAccordion {...defaultProps} />);
|
||||
render(<SectionPanel {...defaultProps} />);
|
||||
expect(screen.getByRole('link', { name: /01.*About/i })).toHaveAttribute('href', '/about');
|
||||
});
|
||||
|
||||
it('does not render children', () => {
|
||||
render(<SectionAccordion {...defaultProps} />);
|
||||
render(<SectionPanel {...defaultProps} />);
|
||||
expect(screen.queryByText('Content here')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render a button', () => {
|
||||
render(<SectionAccordion {...defaultProps} />);
|
||||
render(<SectionPanel {...defaultProps} />);
|
||||
expect(screen.queryByRole('button')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -42,17 +42,17 @@ describe('SectionAccordion', () => {
|
||||
const activeProps = { ...defaultProps, isActive: true };
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
it('renders children', () => {
|
||||
render(<SectionAccordion {...activeProps} />);
|
||||
render(<SectionPanel {...activeProps} />);
|
||||
expect(screen.getByText('Content here')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render a link', () => {
|
||||
render(<SectionAccordion {...activeProps} />);
|
||||
render(<SectionPanel {...activeProps} />);
|
||||
expect(screen.queryByRole('link')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
+2
-2
@@ -2,7 +2,7 @@ import Link from 'next/link';
|
||||
import type { ReactNode } from 'react';
|
||||
import { ViewTransitionWrapper } from '$shared/ui';
|
||||
|
||||
interface SectionAccordionProps {
|
||||
interface SectionPanelProps {
|
||||
/**
|
||||
* Display number prefix (e.g. "01")
|
||||
*/
|
||||
@@ -32,7 +32,7 @@ interface SectionAccordionProps {
|
||||
/**
|
||||
* 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}`;
|
||||
|
||||
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 type { SectionRecord } from '$entities/Section';
|
||||
import { SectionsAccordion } from './SectionsAccordion';
|
||||
import { SectionAccordion } from './SectionAccordion';
|
||||
|
||||
const baseRecord = { collectionId: 'c1', collectionName: 'sections', created: '', updated: '' };
|
||||
|
||||
@@ -10,26 +10,26 @@ const sections: SectionRecord[] = [
|
||||
{ ...baseRecord, id: '3', slug: 'skills', title: 'Skills', order: 3 },
|
||||
];
|
||||
|
||||
describe('SectionsAccordion', () => {
|
||||
describe('SectionAccordion', () => {
|
||||
describe('active section rendering', () => {
|
||||
it('renders the active section as an h1', () => {
|
||||
render(
|
||||
<SectionsAccordion sections={sections} activeSlug="intro">
|
||||
<SectionAccordion sections={sections} activeSlug="intro">
|
||||
<div>Intro content</div>
|
||||
<div>Bio content</div>
|
||||
<div>Skills content</div>
|
||||
</SectionsAccordion>,
|
||||
</SectionAccordion>,
|
||||
);
|
||||
expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent('01. Intro');
|
||||
});
|
||||
|
||||
it('renders inactive sections as links', () => {
|
||||
render(
|
||||
<SectionsAccordion sections={sections} activeSlug="intro">
|
||||
<SectionAccordion sections={sections} activeSlug="intro">
|
||||
<div>Intro content</div>
|
||||
<div>Bio content</div>
|
||||
<div>Skills content</div>
|
||||
</SectionsAccordion>,
|
||||
</SectionAccordion>,
|
||||
);
|
||||
const links = screen.getAllByRole('link');
|
||||
expect(links).toHaveLength(2);
|
||||
@@ -37,11 +37,11 @@ describe('SectionsAccordion', () => {
|
||||
|
||||
it('inactive section links point to correct hrefs', () => {
|
||||
render(
|
||||
<SectionsAccordion sections={sections} activeSlug="intro">
|
||||
<SectionAccordion sections={sections} activeSlug="intro">
|
||||
<div>Intro content</div>
|
||||
<div>Bio content</div>
|
||||
<div>Skills content</div>
|
||||
</SectionsAccordion>,
|
||||
</SectionAccordion>,
|
||||
);
|
||||
expect(screen.getByRole('link', { name: /02.*Bio/i })).toHaveAttribute('href', '/bio');
|
||||
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', () => {
|
||||
render(
|
||||
<SectionsAccordion sections={sections} activeSlug="bio">
|
||||
<SectionAccordion sections={sections} activeSlug="bio">
|
||||
<div>Intro content</div>
|
||||
<div>Bio content</div>
|
||||
<div>Skills content</div>
|
||||
</SectionsAccordion>,
|
||||
</SectionAccordion>,
|
||||
);
|
||||
expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent('02. Bio');
|
||||
});
|
||||
|
||||
it('only one section is active at a time', () => {
|
||||
render(
|
||||
<SectionsAccordion sections={sections} activeSlug="skills">
|
||||
<SectionAccordion sections={sections} activeSlug="skills">
|
||||
<div>Intro content</div>
|
||||
<div>Bio content</div>
|
||||
<div>Skills content</div>
|
||||
</SectionsAccordion>,
|
||||
</SectionAccordion>,
|
||||
);
|
||||
expect(screen.getAllByRole('heading', { level: 1 })).toHaveLength(1);
|
||||
});
|
||||
@@ -73,22 +73,22 @@ describe('SectionsAccordion', () => {
|
||||
describe('content slots', () => {
|
||||
it('shows active section content', () => {
|
||||
render(
|
||||
<SectionsAccordion sections={sections} activeSlug="intro">
|
||||
<SectionAccordion sections={sections} activeSlug="intro">
|
||||
<div>Intro content</div>
|
||||
<div>Bio content</div>
|
||||
<div>Skills content</div>
|
||||
</SectionsAccordion>,
|
||||
</SectionAccordion>,
|
||||
);
|
||||
expect(screen.getByText('Intro content')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not show inactive section content', () => {
|
||||
render(
|
||||
<SectionsAccordion sections={sections} activeSlug="intro">
|
||||
<SectionAccordion sections={sections} activeSlug="intro">
|
||||
<div>Intro content</div>
|
||||
<div>Bio content</div>
|
||||
<div>Skills content</div>
|
||||
</SectionsAccordion>,
|
||||
</SectionAccordion>,
|
||||
);
|
||||
expect(screen.queryByText('Bio content')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Skills content')).not.toBeInTheDocument();
|
||||
+4
-4
@@ -1,7 +1,7 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { Children } from 'react';
|
||||
import type { SectionRecord } from '$entities/Section';
|
||||
import { SectionAccordion } from '$entities/Section';
|
||||
import { SectionPanel } from '$entities/Section';
|
||||
|
||||
export interface Props {
|
||||
/**
|
||||
@@ -24,13 +24,13 @@ export interface Props {
|
||||
* Active section is determined by the URL (activeSlug prop); inactive sections
|
||||
* 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);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{sections.map((section, i) => (
|
||||
<SectionAccordion
|
||||
<SectionPanel
|
||||
key={section.slug}
|
||||
id={section.slug}
|
||||
number={String(section.order).padStart(2, '0')}
|
||||
@@ -39,7 +39,7 @@ export function SectionsAccordion({ sections, activeSlug, children }: Props) {
|
||||
href={`/${section.slug}`}
|
||||
>
|
||||
{slots[i]}
|
||||
</SectionAccordion>
|
||||
</SectionPanel>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
@@ -1 +0,0 @@
|
||||
export { SectionsAccordion } from './ui/SectionsAccordion/SectionsAccordion';
|
||||
Reference in New Issue
Block a user