diff --git a/app/Navbar.css b/app/Navbar.css index 1b8c4c8..86272f5 100644 --- a/app/Navbar.css +++ b/app/Navbar.css @@ -1,25 +1,25 @@ -nav { - background-color: rgba(75, 207, 141, 0.473); - color: black; -} - -ul { - list-style-type: none; - margin: 1rem; - padding: 0; - display: flex; - justify-content: space-around; -} - -li { - padding: 15px; -} - -a { - color: black; - text-decoration: none; -} - -a:hover { - text-decoration: underline; -} +nav { + background-color: rgba(75, 207, 141, 0.473); + color: black; +} + +ul { + list-style-type: none; + margin: 1rem; + padding: 0; + display: flex; + justify-content: space-around; +} + +li { + padding: 15px; +} + +a { + color: black; + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} diff --git a/app/Navbar.tsx b/app/Navbar.tsx index 666e49a..a13d528 100644 --- a/app/Navbar.tsx +++ b/app/Navbar.tsx @@ -1,22 +1,22 @@ -import React from 'react' -import './Navbar.css' - -type navbarProps = { - links: { text: string; url: string }[] -} - -const Navbar: React.FC = ({ links }) => { - return ( - - ) -} - -export default Navbar +import React from 'react'; +import './Navbar.css'; + +type navbarProps = { + links: { text: string; url: string }[]; +}; + +const Navbar: React.FC = ({ links }) => { + return ( + + ); +}; + +export default Navbar; diff --git a/app/globals.css b/app/globals.css index b1b960d..5972a07 100644 --- a/app/globals.css +++ b/app/globals.css @@ -38,11 +38,7 @@ p { align-items: end; padding: 1.5rem 1.5rem; background: - linear-gradient( - 15deg, - transparent 0 22%, - rgba(75, 194, 188, 0.44) 82% 100% - ), + linear-gradient(15deg, transparent 0 22%, rgba(75, 194, 188, 0.44) 82% 100%), var(--accent-soft); } diff --git a/app/layout.tsx b/app/layout.tsx index 1fd14f7..8b87e99 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -6,21 +6,23 @@ import Navbar from './Navbar' export const metadata: Metadata = { title: 'SideCause', - description: 'Complete community service projects and level up!', + description: + 'Complete community service projects and level up!', } + const navLinks = [ { text: 'SideCause', url: '/' }, - { text: 'Posts', url: '/' }, - { text: 'Login', url: '/' }, -] + { text: 'Posts', url: '/posts' }, + { text: 'Login', url: '/' } +]; export default function RootLayout({ children, }: Readonly<{ children: ReactNode }>) { return ( - +
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 deleted file mode 100644 index ac48aef..0000000 --- a/next-env.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -/// -/// -/// -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() + }) +})