generated from michellepace/nextjs-base
-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add right-sidebar content with top questions and popular tags #24
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
99ed11f
refactor: standardise navigation file naming and relocate right-sidebar
michellepace 1b0189f
chore: install shadcn Badge component
michellepace d6a2a99
rules: allow additional Playwright and CLI commands
michellepace b2e852b
feat: add right-sidebar content with data layer
michellepace 37d63da
docs: add microinteractions guide for Next.js 16
michellepace c660171
style: align tag badge colours to Figma target design
michellepace d9b3e95
style: replace QuestionLink hover overlay with microinteractions
michellepace 0504866
style: replace TagLink hover overlay with lift microinteraction
michellepace c9ea632
docs: add right-sidebar implementation handover notes
michellepace d45c672
style: use British spelling in mock question data
michellepace 52e2d15
test: add right sidebar e2e and unit tests, remove demo tests
michellepace File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| type QuestionPageProps = { | ||
| params: Promise<{ id: string }>; | ||
| }; | ||
|
|
||
| export default async function QuestionPage({ params }: QuestionPageProps) { | ||
| const { id } = await params; | ||
| return <h1 className="text-heading-2xl">Question {id}</h1>; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| type TagPageProps = { | ||
| params: Promise<{ slug: string }>; | ||
| }; | ||
|
|
||
| export default async function TagPage({ params }: TagPageProps) { | ||
| const { slug } = await params; | ||
| return <h1 className="text-heading-2xl">Tag: {slug}</h1>; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
File renamed without changes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| import { ChevronRight } from "lucide-react"; | ||
| import Link from "next/link"; | ||
|
|
||
| type QuestionLinkProps = { | ||
| id: string; | ||
| title: string; | ||
| }; | ||
|
|
||
| export function QuestionLink({ id, title }: QuestionLinkProps) { | ||
| return ( | ||
| <Link | ||
| href={`/question/${id}`} | ||
| className="group -mx-2 flex items-center gap-2 rounded-md p-2" | ||
| > | ||
| <span className="flex-1 text-sm leading-snug text-foreground decoration-muted-foreground/50 underline-offset-2 group-hover:underline"> | ||
| {title} | ||
| </span> | ||
| <ChevronRight className="size-4 shrink-0 text-muted-foreground transition-transform duration-150 group-hover:translate-x-0.5" /> | ||
| </Link> | ||
| ); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| import Link from "next/link"; | ||
| import { Badge } from "@/components/ui/badge"; | ||
|
|
||
| type TagLinkProps = { | ||
| name: string; | ||
| questionCount?: number; | ||
| }; | ||
|
|
||
| export function TagLink({ name, questionCount }: TagLinkProps) { | ||
| return ( | ||
| <Link | ||
| href={`/tags/${name}`} | ||
| className="group flex items-center justify-between gap-2 rounded-md p-1 transition-transform duration-150 hover:-translate-y-0.5 hover:scale-[1.005]" | ||
| > | ||
| <Badge className="rounded-md border-transparent bg-(--tag-bg) px-4 py-2 uppercase text-(--tag-text)"> | ||
| {name} | ||
| </Badge> | ||
| {questionCount !== undefined && ( | ||
| <span className="text-xs text-muted-foreground">{questionCount}</span> | ||
| )} | ||
| </Link> | ||
| ); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| import { Slot } from "@radix-ui/react-slot"; | ||
| import { cva, type VariantProps } from "class-variance-authority"; | ||
| import type * as React from "react"; | ||
|
|
||
| import { cn } from "@/lib/utils"; | ||
|
|
||
| const badgeVariants = cva( | ||
| "inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", | ||
| { | ||
| variants: { | ||
| variant: { | ||
| default: | ||
| "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", | ||
| secondary: | ||
| "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", | ||
| destructive: | ||
| "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", | ||
| outline: | ||
| "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", | ||
| }, | ||
| }, | ||
| defaultVariants: { | ||
| variant: "default", | ||
| }, | ||
| }, | ||
| ); | ||
|
|
||
| function Badge({ | ||
| className, | ||
| variant, | ||
| asChild = false, | ||
| ...props | ||
| }: React.ComponentProps<"span"> & | ||
| VariantProps<typeof badgeVariants> & { asChild?: boolean }) { | ||
| const Comp = asChild ? Slot : "span"; | ||
|
|
||
| return ( | ||
| <Comp | ||
| data-slot="badge" | ||
| className={cn(badgeVariants({ variant }), className)} | ||
| {...props} | ||
| /> | ||
| ); | ||
| } | ||
|
|
||
| export { Badge, badgeVariants }; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| import { expect, test } from "@playwright/test"; | ||
|
|
||
| test.describe("Right Sidebar - Desktop", () => { | ||
| test("displays questions and tags with working navigation and responsive scroll", async ({ | ||
| page, | ||
| }) => { | ||
| // Start at short viewport (1280x650) - triggers scrollbar | ||
| await page.setViewportSize({ width: 1280, height: 650 }); | ||
| await page.goto("/"); | ||
|
|
||
| // Sidebar should be visible at xl breakpoint | ||
| const sidebar = page.getByRole("complementary", { | ||
| name: "Top questions and popular tags", | ||
| }); | ||
| await expect(sidebar).toBeVisible(); | ||
|
|
||
| // Count exactly 5 question links and 5 tag links | ||
| const questionLinks = sidebar.locator('a[href^="/question/"]'); | ||
| const tagLinks = sidebar.locator('a[href^="/tags/"]'); | ||
| await expect(questionLinks).toHaveCount(5); | ||
| await expect(tagLinks).toHaveCount(5); | ||
|
|
||
| // Verify scrollbar IS present at short viewport | ||
| const hasScrollAtShortHeight = await page.evaluate(() => { | ||
| const container = document.querySelector( | ||
| '#right-sidebar [data-sidebar="content"]', | ||
| ); | ||
| return container | ||
| ? container.scrollHeight > container.clientHeight | ||
| : false; | ||
| }); | ||
| expect( | ||
| hasScrollAtShortHeight, | ||
| "Sidebar should have vertical scroll at 650px height", | ||
| ).toBe(true); | ||
|
|
||
| // Click first question link and verify URL pattern | ||
| await questionLinks.first().click(); | ||
| await expect(page).toHaveURL(/\/question\/\d+/); | ||
|
|
||
| // Navigate back to homepage | ||
| await page.goto("/"); | ||
|
|
||
| // Click first tag link and verify URL pattern | ||
| await tagLinks.first().click(); | ||
| await expect(page).toHaveURL(/\/tags\/[a-z-]+/); | ||
|
|
||
| // Navigate back to homepage | ||
| await page.goto("/"); | ||
|
|
||
| // Resize to tall viewport (1280x1000) - no scrollbar needed | ||
| await page.setViewportSize({ width: 1280, height: 1000 }); | ||
| const hasScrollAtTallHeight = await page.evaluate(() => { | ||
| const container = document.querySelector( | ||
| '#right-sidebar [data-sidebar="content"]', | ||
| ); | ||
| return container | ||
| ? container.scrollHeight > container.clientHeight | ||
| : false; | ||
| }); | ||
| expect( | ||
| hasScrollAtTallHeight, | ||
| "Sidebar should not have vertical scroll at 1000px height", | ||
| ).toBe(false); | ||
|
|
||
| // Resize to below xl breakpoint (1279px) - sidebar should be hidden | ||
| await page.setViewportSize({ width: 1279, height: 650 }); | ||
| await expect(sidebar).not.toBeVisible(); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| import { getTopQuestions } from "@/lib/data/questions"; | ||
|
|
||
| describe("getTopQuestions", () => { | ||
| it("returns 5 questions by default", async () => { | ||
| const questions = await getTopQuestions(); | ||
| expect(questions).toHaveLength(5); | ||
| }); | ||
|
|
||
| it("respects custom limit parameter", async () => { | ||
| const questions = await getTopQuestions(3); | ||
| expect(questions).toHaveLength(3); | ||
| }); | ||
|
|
||
| it("returns questions with required properties", async () => { | ||
| const questions = await getTopQuestions(1); | ||
| expect(questions[0]).toHaveProperty("_id"); | ||
| expect(questions[0]).toHaveProperty("title"); | ||
| }); | ||
| }); |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.