From efbede62a5947540619446ab523b01a284e6ff42 Mon Sep 17 00:00:00 2001 From: lhoward3306 Date: Wed, 7 Oct 2026 12:15:41 -0700 Subject: [PATCH] Posts Page Setup --- app/layout.tsx | 2 +- app/posts/PostsList.tsx | 37 ++++++++++ app/posts/page.tsx | 26 +++++++ app/posts/post-data.ts | 24 +++++++ app/posts/posts.module.css | 105 +++++++++++++++++++++++++++++ next-env.d.ts | 5 +- tests/e2e/posts-navigation.spec.ts | 29 ++++++++ tests/unit/posts-list.test.tsx | 36 ++++++++++ 8 files changed, 260 insertions(+), 4 deletions(-) create mode 100644 app/posts/PostsList.tsx create mode 100644 app/posts/page.tsx create mode 100644 app/posts/post-data.ts create mode 100644 app/posts/posts.module.css create mode 100644 tests/e2e/posts-navigation.spec.ts create mode 100644 tests/unit/posts-list.test.tsx diff --git a/app/layout.tsx b/app/layout.tsx index e9505b6..8b87e99 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -13,7 +13,7 @@ export const metadata: Metadata = { const navLinks = [ { text: 'SideCause', url: '/' }, - { text: 'Posts', url: '/' }, + { text: 'Posts', url: '/posts' }, { text: 'Login', url: '/' } ]; diff --git a/app/posts/PostsList.tsx b/app/posts/PostsList.tsx new file mode 100644 index 0000000..b58d0a1 --- /dev/null +++ b/app/posts/PostsList.tsx @@ -0,0 +1,37 @@ +import styles from './posts.module.css' + +export type PostPreview = { + id: string + title: string + location: string +} + +type PostsListProps = { + posts: PostPreview[] +} + +export function PostsList({ posts }: PostsListProps) { + if (posts.length === 0) { + return ( +

+ No posts +

+ ) + } + + return ( + + ) +} diff --git a/app/posts/page.tsx b/app/posts/page.tsx new file mode 100644 index 0000000..713a0e3 --- /dev/null +++ b/app/posts/page.tsx @@ -0,0 +1,26 @@ +import { PostsList } from './PostsList' +import { samplePosts } from './post-data' +import styles from './posts.module.css' + +export default function PostsPage() { + return ( +
+
+

+ Posts +

+
+ + +
+
+ +
+
+
+ ) +} diff --git a/app/posts/post-data.ts b/app/posts/post-data.ts new file mode 100644 index 0000000..af3af3d --- /dev/null +++ b/app/posts/post-data.ts @@ -0,0 +1,24 @@ +import type { PostPreview } from './PostsList' + +export const samplePosts: PostPreview[] = [ + { + id: 'cedar-grove-cleanup', + title: 'Clean up Cedar Grove', + location: 'Chico, CA', + }, + { + id: 'food-pantry-sorting', + title: 'Sort food pantry donations', + location: 'Downtown Chico, CA', + }, + { + id: 'community-garden', + title: 'Water the community garden', + location: 'South Chico, CA', + }, + { + id: 'trailhead-tidy', + title: 'Tidy the trailhead', + location: 'Upper Park, Chico, CA', + }, +] diff --git a/app/posts/posts.module.css b/app/posts/posts.module.css new file mode 100644 index 0000000..920ff9d --- /dev/null +++ b/app/posts/posts.module.css @@ -0,0 +1,105 @@ +.postsPage { + width: min(100%, 80rem); + margin: 0 auto; + padding: 1.5rem; +} + +.postsBoard { + padding: 1.25rem; + border: 1px solid var(--ink); + background: #fbfaf5; +} + +.postsTitle { + margin: 0; + font-size: clamp(2.25rem, 9vw, 4rem); +} + +.controls { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0.75rem; + margin: 1.25rem 0; +} + +.controls button, +.postCard button { + font: inherit; + color: var(--ink); + border: 1px solid var(--ink); + border-radius: 0; + background: var(--canvas); +} + +.controls button { + min-height: 2.75rem; + padding: 0.5rem 0.75rem; + text-align: left; +} + +.controls button:disabled, +.postCard button:disabled { + cursor: not-allowed; + opacity: 1; +} + +.listViewport { + height: clamp(19rem, 58dvh, 30rem); + overflow-y: auto; + border-top: 1px solid var(--ink); + border-bottom: 1px solid var(--ink); + scrollbar-color: var(--accent) var(--canvas); +} + +.postsList { + display: grid; + gap: 0.75rem; + margin: 0; + padding: 0.75rem 0; + list-style: none; +} + +.postCard { + display: flex; + align-items: end; + justify-content: space-between; + gap: 1rem; + min-height: 7.5rem; + padding: 1rem; + border: 1px solid var(--ink); + background: #fffefa; +} + +.postCard h2 { + margin: 0; + font-size: clamp(1.25rem, 4vw, 1.75rem); +} + +.postCard p { + margin-top: 0.5rem; + color: var(--muted); +} + +.postCard button { + flex: 0 0 auto; + padding: 0.5rem 0.75rem; + white-space: nowrap; +} + +.emptyState { + display: grid; + height: 100%; + margin: 0; + place-items: center; + color: var(--muted); +} + +@media (min-width: 48rem) { + .postsPage { + padding: 3rem; + } + + .postsBoard { + padding: 1.5rem; + } +} diff --git a/next-env.d.ts b/next-env.d.ts index 304d663..a419cbe 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,8 +1,7 @@ /// /// -/// -import './.next/dev/types/routes.d.ts' -import './.next/dev/types/root-params.d.ts' +import "./.next/dev/types/routes.d.ts"; +import "./.next/dev/types/root-params.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/tests/e2e/posts-navigation.spec.ts b/tests/e2e/posts-navigation.spec.ts new file mode 100644 index 0000000..b4235df --- /dev/null +++ b/tests/e2e/posts-navigation.spec.ts @@ -0,0 +1,29 @@ +import { expect, test } from '@playwright/test' + +test('opens the Posts page from the homepage navigation', async ({ page }) => { + await page.goto('/') + + await page.getByRole('link', { name: 'Posts' }).click() + + await expect(page).toHaveURL('/posts') + await expect(page.getByRole('heading', { name: 'Posts' })).toBeVisible() +}) + +test('expands the posts board for desktop while keeping it within a narrow viewport', async ({ + page, +}) => { + await page.setViewportSize({ width: 1440, height: 900 }) + await page.goto('/posts') + + const board = page.locator('section[aria-labelledby="posts-title"]') + const desktopBoard = await board.boundingBox() + + expect(desktopBoard?.width).toBeGreaterThan(1000) + + await page.setViewportSize({ width: 320, height: 900 }) + + const narrowBoard = await board.boundingBox() + + expect(narrowBoard?.width).toBeGreaterThan(200) + expect(narrowBoard?.width).toBeLessThanOrEqual(320) +}) diff --git a/tests/unit/posts-list.test.tsx b/tests/unit/posts-list.test.tsx new file mode 100644 index 0000000..adc2afa --- /dev/null +++ b/tests/unit/posts-list.test.tsx @@ -0,0 +1,36 @@ +// @vitest-environment jsdom + +import { render, screen } from '@testing-library/react' +import { describe, expect, it } from 'vitest' + +import { PostsList } from '../../app/posts/PostsList' + +describe('PostsList', () => { + it('renders each post with its title, location, and future detail action', () => { + render( + , + ) + + expect(screen.getByText('Park cleanup')).toBeTruthy() + expect(screen.getByText('Bidwell Park')).toBeTruthy() + expect(screen.getByText('Food drive')).toBeTruthy() + expect( + screen.getAllByRole('button', { name: 'View details' }), + ).toHaveLength(2) + }) + + it('shows the empty state when no posts are available', () => { + render() + + expect(screen.getByText('No posts')).toBeTruthy() + }) +})