feat: Next.js 14 frontend — complete blog platform UI#85
Conversation
25 files — full production-ready Next.js 14 App Router frontend. Tech stack: - Next.js 14.2 + TypeScript (strict mode, no `any`) - Tailwind CSS (dark neutral-950 palette, sky-blue brand accents) - React Context for auth state — no external state library - JWT: access token in memory, refresh token in localStorage, silent auto-refresh on 401 Library layer (src/lib/): - types.ts — TypeScript interfaces for all API response shapes - auth.ts — token storage, login, logout, silent refresh - api.ts — typed fetch wrapper with auto-refresh, organized by domain - AuthContext.tsx — React Context + useAuth() hook Components: - Navbar, PostCard, PostList, SearchBar, ProtectedRoute Pages: - / public feed, /login, /register, /posts/[slug], /dashboard, /dashboard/new, /profile/[handle], /notifications Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds a new frontend/ Next.js 14 (App Router) UI intended to consume the existing Django REST API, including auth/session handling, typed API wrappers, and core pages (feed, auth, dashboard, post detail, profile, notifications).
Changes:
- Introduces a typed API client + auth/session utilities (
src/lib/*) and shared UI components. - Implements primary routes/pages for the blog platform in the Next.js App Router.
- Adds Tailwind styling + project configuration (TS, Tailwind, PostCSS, Next config, env example).
Reviewed changes
Copilot reviewed 26 out of 26 changed files in this pull request and generated 22 comments.
Show a summary per file
| File | Description |
|---|---|
| frontend/tsconfig.json | TypeScript strict config for the new frontend. |
| frontend/tailwind.config.ts | Tailwind theme/colors/animations configuration. |
| frontend/src/lib/types.ts | Defines TypeScript interfaces for API payloads and helpers. |
| frontend/src/lib/AuthContext.tsx | Auth context/provider and useAuth() hook for session state. |
| frontend/src/lib/auth.ts | Access/refresh token storage + refresh logic. |
| frontend/src/lib/api.ts | Typed fetch wrapper and domain API clients. |
| frontend/src/components/SearchBar.tsx | Search input component with clear button. |
| frontend/src/components/ProtectedRoute.tsx | Client-side auth gate for protected pages. |
| frontend/src/components/PostList.tsx | Feed list rendering + pagination UI. |
| frontend/src/components/PostCard.tsx | Post list item card UI and navigation. |
| frontend/src/components/Navbar.tsx | Top navigation with auth-aware links and mobile menu. |
| frontend/src/app/register/page.tsx | Registration page with validation + strength meter. |
| frontend/src/app/profile/[handle]/page.tsx | Public profile page with follow/unfollow + posts listing. |
| frontend/src/app/posts/[slug]/page.tsx | Post detail page + comment thread and comment form. |
| frontend/src/app/page.tsx | Home feed page with search/tag filters + trending sidebar. |
| frontend/src/app/notifications/page.tsx | Notifications list UI with filter + mark-all-read action. |
| frontend/src/app/login/page.tsx | Login page and token-based sign-in flow. |
| frontend/src/app/layout.tsx | Root layout wiring (AuthProvider, Navbar, global metadata). |
| frontend/src/app/globals.css | Tailwind base + global styling utilities and “article” prose styles. |
| frontend/src/app/dashboard/page.tsx | Dashboard page listing user posts with filters and delete. |
| frontend/src/app/dashboard/new/page.tsx | New post creation form with status/visibility and previews. |
| frontend/postcss.config.js | PostCSS config for Tailwind. |
| frontend/package.json | Frontend dependencies and scripts. |
| frontend/next.config.ts | Next.js config (remote image patterns). |
| frontend/.gitignore | Frontend-specific ignores. |
| frontend/.env.local.example | Example env vars for local dev. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const BASE = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:8000"; | ||
|
|
There was a problem hiding this comment.
The frontend hard-codes an /api/v1 prefix (e.g., /api/v1/token/, /api/v1/public/posts/), but the Django backend routes are under /api/... (e.g., /api/token/, /api/public/posts/). As-is, every request will 404 against the current backend; consider making the prefix configurable or updating these paths to match the backend URL patterns.
| me(): Promise<MeResponse> { | ||
| return apiFetch<MeResponse>("/api/v1/auth/me/"); | ||
| }, |
There was a problem hiding this comment.
authApi.me() calls /api/v1/auth/me/, but the backend exposes the authenticated user profile at /api/auth/profile/ (and does not have an /auth/me/ route). This breaks session restoration and any UI that depends on me(); update the endpoint and expected response shape accordingly.
| markAllRead(): Promise<void> { | ||
| return apiFetch<void>("/api/v1/notifications/mark-read/", { | ||
| method: "PATCH", | ||
| }); | ||
| }, |
There was a problem hiding this comment.
notificationsApi.markAllRead() posts to /api/v1/notifications/mark-read/, but the backend only defines GET /api/notifications/ (no mark-all-read endpoint). This call will 404; either add the endpoint server-side or remove/feature-flag this client action and update the UI accordingly.
| list(page = 1): Promise<PaginatedResponse<MyPost>> { | ||
| return apiFetch<PaginatedResponse<MyPost>>(`/api/v1/posts/?page=${page}`); |
There was a problem hiding this comment.
postsApi.list() expects a paginated {count, results, ...} response, but the backend /api/posts/ route is a DRF ModelViewSet without pagination_class or a global default, so it returns an array. Either add pagination server-side or adjust the client types and list handling to accept a plain array.
| list(page = 1): Promise<PaginatedResponse<MyPost>> { | |
| return apiFetch<PaginatedResponse<MyPost>>(`/api/v1/posts/?page=${page}`); | |
| list(page = 1): Promise<MyPost[]> { | |
| return apiFetch<MyPost[]>(`/api/v1/posts/?page=${page}`); |
| {/* Stretch link overlay */} | ||
| <Link href={`/posts/${post.slug}`} className="absolute inset-0 rounded-xl" aria-hidden="true" tabIndex={-1} /> | ||
| </article> |
There was a problem hiding this comment.
The absolute-positioned “stretch link overlay” is rendered last and covers the entire card (absolute inset-0), which will intercept clicks meant for inner links (e.g., the author profile link) and buttons. Consider using z-index layering (e.g., content above, overlay below) or pointer-events-none on the overlay so inner interactive elements remain usable.
| }, | ||
|
|
||
| stats(id: number): Promise<UserStats> { | ||
| return apiFetch<UserStats>(`/api/v1/users/${id}/stats/`); |
There was a problem hiding this comment.
usersApi.stats() calls /api/v1/users/${id}/stats/, but the backend only provides GET /api/users/<id>/ (no /stats/ route). This will 404; either add a stats endpoint server-side or remove/adjust this client call.
| return apiFetch<UserStats>(`/api/v1/users/${id}/stats/`); | |
| return apiFetch<UserStats>(`/api/users/${id}/`); |
| const data = await publicApi.feed({ | ||
| page: currentPage, | ||
| search: search || undefined, | ||
| tag: activeTag || undefined, | ||
| ordering: "-published_at", | ||
| }); | ||
| if (!cancelled) { |
There was a problem hiding this comment.
This passes a tag query parameter for filtering, but the backend PublicPostListView only supports DRF SearchFilter and OrderingFilter and does not implement tag filtering. The UI chips will not work as described unless the API adds tag filtering or the client changes to match the supported query params.
| // Redirect if already logged in | ||
| if (!authLoading && isAuthenticated) { | ||
| router.push("/dashboard"); | ||
| return null; | ||
| } |
There was a problem hiding this comment.
This redirect is triggered during render (router.push(...) inside the component body). In React/Next.js this can cause warnings and double navigations; it should be done in a useEffect once authLoading resolves (or handled via a server-side redirect() if converting to a server component).
| if (!authLoading && isAuthenticated) { | ||
| router.push("/dashboard"); | ||
| return null; | ||
| } |
There was a problem hiding this comment.
This redirect is triggered during render (router.push(...) inside the component body). In React/Next.js this can cause warnings and double navigations; it should be done in a useEffect once authLoading resolves (or handled via a server-side redirect() if converting to a server component).
| const REFRESH_KEY = "blog_refresh_token"; | ||
| const API_URL = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:8000"; | ||
|
|
||
| // Module-level memory store for the access token | ||
| let _accessToken: string | null = null; | ||
|
|
There was a problem hiding this comment.
The refresh token is persisted in localStorage, which is readable by any injected script (XSS) and is generally not recommended for long-lived credentials. If this is intended, consider at least documenting the tradeoff prominently; otherwise prefer an httpOnly, sameSite cookie-based refresh token flow.
Summary
Closes #84
Adds a complete, production-ready Next.js 14 frontend for the blog platform API.
Tech Stack
any)useAuth()hook)fetchwith JWT auto-refresh wrapperPages
//login/register/posts/[slug]/dashboard/dashboard/new/profile/[handle]/notificationsArchitecture Highlights
'use client'only on interactive leaves (13/26 files)ProtectedRouteHOC — wraps protected pages, redirects to/loginwith return URLsrc/lib/api.tsorganises all calls by domain (authApi,publicApi,postsApi,notificationsApi,usersApi), all return types matchsrc/lib/types.tsinterfacesGetting Started
Make sure the Django backend is running first:
Test Plan
/posts/[slug]— post detail renders with comments🤖 Generated with Claude Code