Skip to content

feat: Next.js 14 frontend — complete blog platform UI#85

Merged
sadykovIsmail merged 1 commit into
mainfrom
feature/issue-84-nextjs-frontend
Mar 31, 2026
Merged

feat: Next.js 14 frontend — complete blog platform UI#85
sadykovIsmail merged 1 commit into
mainfrom
feature/issue-84-nextjs-frontend

Conversation

@sadykovIsmail

Copy link
Copy Markdown
Owner

Summary

Closes #84

Adds a complete, production-ready Next.js 14 frontend for the blog platform API.


Tech Stack

Layer Technology
Framework Next.js 14 App Router
Language TypeScript (strict, no any)
Styling Tailwind CSS (dark neutral-950, sky-blue brand)
Auth state React Context (useAuth() hook)
HTTP Native fetch with JWT auto-refresh wrapper
Token storage Access: in-memory module variable · Refresh: localStorage

Pages

Route Description Auth
/ Public feed — search, tag filter, trending sidebar, pagination Public
/login JWT login with show/hide password, redirect if already authed Public
/register Registration with live password strength meter Public
/posts/[slug] Post detail with comment thread and inline comment form Public (write requires auth)
/dashboard My posts — status filter tabs, delete with confirmation Protected
/dashboard/new Create post — live word count + reading time preview Protected
/profile/[handle] Public profile — stats, follow/unfollow, paginated posts Public
/notifications Notifications — type icons, all/unread filter, mark-all-read Protected

Architecture Highlights

  • Server components by default'use client' only on interactive leaves (13/26 files)
  • Loading skeletons on every async boundary — no layout shift
  • Error states with user-friendly messages on every fetch
  • JWT silent refresh — 401 response triggers token refresh then retries the original request transparently
  • ProtectedRoute HOC — wraps protected pages, redirects to /login with return URL
  • Typed API layersrc/lib/api.ts organises all calls by domain (authApi, publicApi, postsApi, notificationsApi, usersApi), all return types match src/lib/types.ts interfaces

Getting Started

cd frontend
cp .env.local.example .env.local   # pre-configured for localhost:8000
npm install
npm run dev                         # http://localhost:3000

Make sure the Django backend is running first:

docker compose up --build

Test Plan

  • Home feed loads public posts with search and tag filter working
  • Login → JWT tokens stored, navbar shows authenticated state
  • Register → new account created, auto-login
  • /posts/[slug] — post detail renders with comments
  • Dashboard → lists my posts, status filter works, delete removes post
  • Create post → redirects to dashboard on success
  • Profile page → follow/unfollow button updates in real-time
  • Notifications page → mark-all-read button updates unread count in navbar

🤖 Generated with Claude Code

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>
Copilot AI review requested due to automatic review settings March 31, 2026 00:23
@sadykovIsmail sadykovIsmail merged commit a953fa1 into main Mar 31, 2026
3 of 5 checks passed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread frontend/src/lib/api.ts
Comment on lines +32 to +33
const BASE = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:8000";

Copilot AI Mar 31, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread frontend/src/lib/api.ts
Comment on lines +125 to +127
me(): Promise<MeResponse> {
return apiFetch<MeResponse>("/api/v1/auth/me/");
},

Copilot AI Mar 31, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread frontend/src/lib/api.ts
Comment on lines +214 to +218
markAllRead(): Promise<void> {
return apiFetch<void>("/api/v1/notifications/mark-read/", {
method: "PATCH",
});
},

Copilot AI Mar 31, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread frontend/src/lib/api.ts
Comment on lines +182 to +183
list(page = 1): Promise<PaginatedResponse<MyPost>> {
return apiFetch<PaginatedResponse<MyPost>>(`/api/v1/posts/?page=${page}`);

Copilot AI Mar 31, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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}`);

Copilot uses AI. Check for mistakes.
Comment on lines +123 to +125
{/* Stretch link overlay */}
<Link href={`/posts/${post.slug}`} className="absolute inset-0 rounded-xl" aria-hidden="true" tabIndex={-1} />
</article>

Copilot AI Mar 31, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread frontend/src/lib/api.ts
},

stats(id: number): Promise<UserStats> {
return apiFetch<UserStats>(`/api/v1/users/${id}/stats/`);

Copilot AI Mar 31, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
return apiFetch<UserStats>(`/api/v1/users/${id}/stats/`);
return apiFetch<UserStats>(`/api/users/${id}/`);

Copilot uses AI. Check for mistakes.
Comment thread frontend/src/app/page.tsx
Comment on lines +112 to +118
const data = await publicApi.feed({
page: currentPage,
search: search || undefined,
tag: activeTag || undefined,
ordering: "-published_at",
});
if (!cancelled) {

Copilot AI Mar 31, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +18 to +22
// Redirect if already logged in
if (!authLoading && isAuthenticated) {
router.push("/dashboard");
return null;
}

Copilot AI Mar 31, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines +48 to +51
if (!authLoading && isAuthenticated) {
router.push("/dashboard");
return null;
}

Copilot AI Mar 31, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment thread frontend/src/lib/auth.ts
Comment on lines +13 to +18
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;

Copilot AI Mar 31, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: Next.js 14 frontend — complete blog platform UI

2 participants