diff --git a/NEXTJS_MIGRATION_PLAN.md b/NEXTJS_MIGRATION_PLAN.md new file mode 100644 index 0000000..df8497f --- /dev/null +++ b/NEXTJS_MIGRATION_PLAN.md @@ -0,0 +1,2628 @@ +# TechStacks: Nuxt.js to Next.js 16 Migration Plan + +## Executive Summary + +This document outlines a comprehensive plan to migrate the TechStacks web application from **Nuxt.js 1.4.5 + Vue 2 + Vuetify 1.0** to **Next.js 16 + React 19 + TypeScript + Tailwind CSS v4**, while maintaining full compatibility with the existing C# ServiceStack backend. + +**Current Stack:** +- Frontend: Nuxt.js 1.4.5, Vue 2, Vuetify 1.0, Tailwind CSS v3 +- Backend: ASP.NET Core 8.0, ServiceStack v8, PostgreSQL +- API Client: @servicestack/client 2.0.17 + +**Target Stack:** +- Frontend: Next.js 16, React 19, TypeScript, Tailwind CSS v4 +- Backend: **Unchanged** - ASP.NET Core 8.0, ServiceStack v8 +- API Client: @servicestack/client (same, with TypeScript) + +--- + +## 🎯 CRITICAL: ALL DATA FLOWS THROUGH EXISTING C# APIS + +**This Next.js application is a pure UI layer with ZERO independent data sources.** + +### Data Source Architecture + +✅ **What the Next.js app DOES:** +- Renders beautiful, modern React UI +- Handles client-side routing and navigation +- Manages UI state (loading, modals, forms) +- Caches API responses for performance +- Handles authentication session display + +❌ **What the Next.js app DOES NOT do:** +- ❌ Direct database access +- ❌ Independent REST APIs +- ❌ GraphQL layer +- ❌ Custom data processing/transformation +- ❌ Bypassing ServiceStack in any way + +### Every Single Piece of Data Comes From C# ServiceStack APIs + +| Data Type | Source API Endpoint | Section Reference | +|-----------|-------------------|-------------------| +| **Technologies** | `GetTechnology`, `GetAllTechnologies`, `QueryTechnology` | Section 4.2 | +| **Tech Stacks** | `GetTechnologyStack`, `GetAllTechnologyStacks` | Section 4.2 | +| **Posts** | `QueryPosts`, `GetPost`, `CreatePost`, `UpdatePost` | Section 4.2 | +| **Comments** | `GetPostComments`, `CreatePostComment`, `UpdatePostComment` | Section 4.2 | +| **Organizations** | `GetOrganizationBySlug`, `GetOrganizationById` | Section 4.2 | +| **Users** | `GetUserInfo`, `GetUserFeed`, `GetUserOrganizations` | Section 4.2 | +| **Authentication** | `Authenticate`, `SessionInfo` | Section 6 | +| **Favorites** | `AddFavoriteTechnology`, `RemoveFavoriteTechnology` | Section 4.2 | +| **Votes** | `UserPostVote`, `UserPostCommentVote` | Section 4.2 | +| **Search** | `QueryTechnology`, `QueryTechStacks`, `QueryPosts` | Section 4.2 | +| **Configuration** | `GetConfig`, `Overview` | Section 4.2 | + +### Data Flow Diagram + +``` +┌─────────────────────────────────────────────────┐ +│ Next.js React Components (UI Only) │ +│ ├─ Display data from props/state │ +│ ├─ Handle user interactions │ +│ └─ Trigger API calls via gateway │ +└────────────────┬────────────────────────────────┘ + │ + ↓ +┌─────────────────────────────────────────────────┐ +│ Zustand Store (Client-side Cache Only) │ +│ ├─ Caches API responses temporarily │ +│ ├─ Stores user session from C# API │ +│ └─ NO independent data processing │ +└────────────────┬────────────────────────────────┘ + │ + ↓ +┌─────────────────────────────────────────────────┐ +│ Gateway Layer (lib/api/gateway.ts) │ +│ ├─ Thin wrapper around JsonServiceClient │ +│ ├─ Maps function calls to DTO requests │ +│ └─ NO business logic │ +└────────────────┬────────────────────────────────┘ + │ + ↓ +┌─────────────────────────────────────────────────┐ +│ @servicestack/client (JsonServiceClient) │ +│ ├─ HTTP calls to /api/* endpoints │ +│ ├─ Serializes DTOs to JSON │ +│ └─ Handles authentication cookies │ +└────────────────┬────────────────────────────────┘ + │ + ↓ +┌─────────────────────────────────────────────────┐ +│ C# ServiceStack Backend ← ALL DATA HERE │ +│ ├─ ServiceStack Services (business logic) │ +│ ├─ AutoQuery (dynamic queries) │ +│ ├─ Validation & Authorization │ +│ ├─ OrmLite + Entity Framework │ +│ └─ PostgreSQL Database │ +└─────────────────────────────────────────────────┘ +``` + +### API Integration Guarantee + +**Every API call in the Next.js app follows this pattern:** + +```typescript +// 1. Import typed DTO from C# project +import { GetTechnology } from '@/shared/dtos'; + +// 2. Import gateway method (wrapper around JsonServiceClient) +import * as gateway from '@/lib/api/gateway'; + +// 3. Call C# API - no alternative data sources +export async function loadTechnology(slug: string) { + // This calls: https://techstacks.io/api/GetTechnology?slug=typescript + const response = await gateway.getTechnology(slug); + return response.result; // Data comes directly from C# API +} +``` + +**The DTOs (`shared/dtos.ts`) are auto-generated from the C# project** - ensuring the Next.js app cannot deviate from the ServiceStack API contract. + +--- + +## Table of Contents + +1. [Project Structure & Setup](#1-project-structure--setup) +2. [Technology Stack & Dependencies](#2-technology-stack--dependencies) +3. [Architecture & Design Patterns](#3-architecture--design-patterns) +4. [API Integration Strategy](#4-api-integration-strategy) +5. [State Management](#5-state-management) +6. [Authentication & Authorization](#6-authentication--authorization) +7. [Routing & Navigation](#7-routing--navigation) +8. [Page Migration Matrix](#8-page-migration-matrix) +9. [Component Migration Strategy](#9-component-migration-strategy) +10. [Styling & UI Framework](#10-styling--ui-framework) +11. [Build & Deployment](#11-build--deployment) +12. [Testing Strategy](#12-testing-strategy) +13. [Migration Phases](#13-migration-phases) +14. [Risk Assessment & Mitigation](#14-risk-assessment--mitigation) + +--- + +## 1. Project Structure & Setup + +### 1.1 New Next.js Directory Structure + +``` +TechStacks/ +├── TechStacks/ # Existing C# project (unchanged) +│ ├── wwwroot/ # Build output destination +│ ├── TechStacks.csproj +│ ├── Program.cs +│ └── ... +│ +├── TechStacks.ServiceModel/ # Existing (unchanged) +├── TechStacks.ServiceInterface/ # Existing (unchanged) +├── TechStacks.Tests/ # Existing (unchanged) +│ +└── nextjs-app/ # NEW: Next.js 16 application + ├── src/ + │ ├── app/ # Next.js App Router (pages) + │ │ ├── layout.tsx # Root layout + │ │ ├── page.tsx # Home page (/) + │ │ ├── top/ # /top route + │ │ ├── tech/ # /tech routes + │ │ ├── stacks/ # /stacks routes + │ │ ├── organizations/ # /organizations routes + │ │ ├── users/ # /users routes + │ │ ├── posts/ # /posts routes + │ │ ├── comments/ # /comments routes + │ │ ├── favorites/ # /favorites routes + │ │ ├── news/ # /news routes + │ │ ├── login/ # /login routes + │ │ └── [slug]/ # Dynamic org routes + │ │ + │ ├── components/ # React components + │ │ ├── ui/ # Base UI components + │ │ ├── forms/ # Form components + │ │ ├── posts/ # Post-related components + │ │ ├── tech/ # Technology components + │ │ ├── stacks/ # Stack components + │ │ ├── organizations/ # Organization components + │ │ └── layout/ # Layout components + │ │ + │ ├── lib/ # Utilities & configuration + │ │ ├── api/ # API client & gateway + │ │ │ ├── client.ts # JsonServiceClient setup + │ │ │ └── gateway.ts # API service methods + │ │ ├── hooks/ # Custom React hooks + │ │ ├── stores/ # State management (Zustand) + │ │ ├── utils/ # Utility functions + │ │ └── types/ # TypeScript types + │ │ + │ ├── shared/ # Shared with backend + │ │ └── dtos.ts # ServiceStack DTOs (copied from C#) + │ │ + │ └── styles/ # Global styles + │ ├── globals.css # Tailwind imports + global styles + │ ├── typography.css # Typography styles + │ └── markdown.css # GitHub Flavored Markdown + │ + ├── public/ # Static assets + │ ├── favicon.ico + │ └── images/ + │ + ├── next.config.ts # Next.js configuration + ├── tailwind.config.ts # Tailwind CSS v4 config + ├── tsconfig.json # TypeScript configuration + ├── package.json # Dependencies + └── .env.local # Environment variables +``` + +### 1.2 Initial Setup Commands + +```bash +# Create Next.js 16 app with TypeScript and Tailwind +cd TechStacks +npx create-next-app@16 nextjs-app --typescript --tailwind --app --src-dir + +# Navigate to new app +cd nextjs-app + +# Install dependencies +npm install @servicestack/client +npm install zustand +npm install date-fns +npm install clsx tailwind-merge +npm install react-markdown remark-gfm rehype-raw +npm install @radix-ui/react-dialog @radix-ui/react-dropdown-menu +npm install @radix-ui/react-select @radix-ui/react-checkbox +npm install lucide-react + +# Development dependencies +npm install -D @types/node @types/react @types/react-dom +npm install -D eslint-config-next +npm install -D prettier eslint-config-prettier +``` + +--- + +## 2. Technology Stack & Dependencies + +### 2.1 Core Framework + +| Category | Current (Nuxt) | New (Next.js) | Reason | +|----------|---------------|---------------|---------| +| **Framework** | Nuxt.js 1.4.5 | Next.js 16 | Modern, actively maintained, React Server Components | +| **UI Library** | Vue 2 | React 19 | Latest React with concurrent rendering | +| **Language** | JavaScript + TypeScript | TypeScript (strict) | Full type safety | +| **CSS Framework** | Tailwind CSS v3 + Vuetify 1.0 | Tailwind CSS v4 | Modern utility-first styling | +| **Node Version** | 16.x | 20.x LTS | Current LTS version | + +### 2.2 Key Dependencies + +```json +{ + "dependencies": { + "next": "^16.0.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "@servicestack/client": "^2.0.17", + "zustand": "^5.0.0", + "date-fns": "^4.0.0", + "clsx": "^2.1.0", + "tailwind-merge": "^2.6.0", + "react-markdown": "^9.0.0", + "remark-gfm": "^4.0.0", + "rehype-raw": "^7.0.0", + "@radix-ui/react-dialog": "^1.1.0", + "@radix-ui/react-dropdown-menu": "^2.1.0", + "@radix-ui/react-select": "^2.1.0", + "@radix-ui/react-checkbox": "^1.1.0", + "lucide-react": "^0.460.0" + }, + "devDependencies": { + "@types/node": "^22", + "@types/react": "^19", + "@types/react-dom": "^19", + "typescript": "^5.7", + "tailwindcss": "^4.0.0", + "postcss": "^8", + "eslint": "^9", + "eslint-config-next": "^16.0.0", + "prettier": "^3.4.0" + } +} +``` + +### 2.3 Removed Dependencies + +These Nuxt/Vue-specific packages will no longer be needed: +- `nuxt` (replaced by Next.js) +- `vuetify` (replaced by Tailwind + Radix UI) +- `@nuxtjs/proxy` (replaced by Next.js rewrites) +- `babel-eslint` (replaced by TypeScript ESLint) +- `eslint-plugin-vue` (replaced by eslint-config-next) + +--- + +## 3. Architecture & Design Patterns + +### 3.1 Architectural Approach + +**Next.js 16 App Router Architecture:** + +``` +┌─────────────────────────────────────────────────┐ +│ Browser / Client │ +├─────────────────────────────────────────────────┤ +│ React 19 Components (Client Components) │ +│ ├─ Page Components (app/*/page.tsx) │ +│ ├─ Interactive UI Components │ +│ └─ Client-side State (Zustand) │ +├─────────────────────────────────────────────────┤ +│ Next.js Server Components (RSC) │ +│ ├─ Layout Components (app/*/layout.tsx) │ +│ ├─ Data Fetching (Server Side) │ +│ └─ SEO Metadata Generation │ +├─────────────────────────────────────────────────┤ +│ Next.js API Routes (Optional) │ +│ └─ /api/* for proxying if needed │ +├─────────────────────────────────────────────────┤ +│ ServiceStack JsonServiceClient │ +│ └─ HTTP calls to C# backend │ +├─────────────────────────────────────────────────┤ +│ ASP.NET Core 8.0 + ServiceStack │ +│ ├─ ServiceStack Services │ +│ ├─ Business Logic │ +│ └─ PostgreSQL Database │ +└─────────────────────────────────────────────────┘ +``` + +### 3.2 Design Patterns + +#### **3.2.1 Server Components vs Client Components** + +**Use Server Components (RSC) for:** +- Initial data fetching (SEO-friendly) +- Static layouts and navigation +- Non-interactive content display +- Metadata generation + +**Use Client Components for:** +- Interactive forms and inputs +- User authentication state +- Real-time updates (voting, favorites) +- Modal dialogs and dropdowns +- Client-side filtering/sorting + +**Example Pattern:** +```tsx +// app/tech/[slug]/page.tsx - Server Component (default) +export default async function TechnologyPage({ params }) { + // Fetch initial data on server for SEO + const tech = await fetchTechnology(params.slug); + + return ( +
+ {/* Server component */} + {/* Client component */} +
+ ); +} + +// components/tech/TechnologyComments.tsx - Client Component +'use client' +export function TechnologyComments({ techId }: { techId: number }) { + const [comments, setComments] = useState([]); + // Interactive comment functionality +} +``` + +#### **3.2.2 State Management Strategy** + +**Three-tier state approach:** + +1. **Server State** (React Query alternative with Zustand) + - API response caching + - Background refetching + - Optimistic updates + +2. **Global Client State** (Zustand) + - User session/authentication + - User favorites and votes + - Global UI state (modals, shortcuts) + +3. **Local Component State** (useState/useReducer) + - Form inputs + - UI toggles + - Component-specific state + +#### **3.2.3 API Integration Pattern** + +**Gateway Service Layer:** +```typescript +// lib/api/gateway.ts +import { JsonServiceClient } from '@servicestack/client'; +import { GetTechnology, CreatePost } from '@/shared/dtos'; + +const client = new JsonServiceClient('/'); + +export const gateway = { + // Technology APIs + getTechnology: (slug: string) => + client.get(new GetTechnology({ slug })), + + getAllTechnologies: () => + client.get(new GetAllTechnologies()), + + // Post APIs with error handling + createPost: async (args, image?) => { + try { + const request = new CreatePost(); + Object.assign(request, args); + const body = image ? toFormData({ ...args, image }) : args; + return await client.postBody(request, body); + } catch (error) { + throw handleApiError(error); + } + } +}; +``` + +**Custom Hooks for Data Fetching:** +```typescript +// lib/hooks/useTechnology.ts +export function useTechnology(slug: string) { + const [tech, setTech] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + gateway.getTechnology(slug) + .then(setTech) + .finally(() => setLoading(false)); + }, [slug]); + + return { tech, loading }; +} +``` + +--- + +## 4. API Integration Strategy + +### 4.1 JsonServiceClient Setup + +**Configuration: `lib/api/client.ts`** + +```typescript +import { JsonServiceClient } from '@servicestack/client'; + +// Base URL configuration +const getBaseUrl = () => { + if (typeof window === 'undefined') { + // Server-side: use internal URL + return process.env.INTERNAL_API_URL || 'https://localhost:5001'; + } + // Client-side: use relative path (served by same origin) + return '/'; +}; + +export const client = new JsonServiceClient(getBaseUrl()); + +// Configure global settings +client.bearerToken = null; // Set dynamically after auth +client.onAuthenticationRequired = () => { + // Redirect to login + if (typeof window !== 'undefined') { + window.location.href = '/login/github'; + } +}; +``` + +### 4.2 Gateway Service Layer + +**Structure: `lib/api/gateway.ts`** + +This file replicates the functionality of `TechStacks/src/shared/gateway.js` but in TypeScript. + +```typescript +import { client } from './client'; +import { toFormData } from '@servicestack/client'; +import * as dtos from '@/shared/dtos'; + +// ============================================ +// CONFIG & OVERVIEW +// ============================================ + +export const getConfig = async () => { + try { + return await client.get(new dtos.GetConfig()); + } catch { + return null; + } +}; + +export const getOverview = async () => { + return await client.get(new dtos.Overview()); +}; + +export const getSessionInfo = async () => { + try { + return await client.get(new dtos.SessionInfo()); + } catch { + return null; + } +}; + +// ============================================ +// AUTHENTICATION +// ============================================ + +export const login = async ( + provider: string, + userName?: string, + password?: string +) => { + await logout(); + + const request = new dtos.Authenticate(); + request.provider = provider; + request.userName = userName; + request.password = password; + + const response = await client.post(request); + + // Set bearer token for subsequent requests + if (response.bearerToken) { + client.bearerToken = response.bearerToken; + } + + await getSessionInfo(); + return `/${provider}`; +}; + +export const logout = async () => { + const request = new dtos.Authenticate(); + request.provider = 'logout'; + await client.post(request); + client.bearerToken = null; +}; + +// ============================================ +// TECHNOLOGIES +// ============================================ + +export const getTechnology = async (slug: string) => { + const request = new dtos.GetTechnology(); + request.slug = slug; + return await client.get(request); +}; + +export const getAllTechnologies = async () => { + return await client.get(new dtos.GetAllTechnologies()); +}; + +export const queryTechnology = async (query: any) => { + return await client.get( + new dtos.QueryTechnology(), + { include: 'total', ...query } + ); +}; + +export const createTechnology = async (args: any, logo?: File) => { + const request = new dtos.CreateTechnology(); + const body = toFormData({ ...args, logo }); + return (await client.postBody(request, body)).result; +}; + +export const updateTechnology = async (args: any, logo?: File) => { + const request = new dtos.UpdateTechnology(); + const body = toFormData({ ...args, logo }); + return (await client.putBody(request, body)).result; +}; + +export const deleteTechnology = async (id: number) => { + const request = new dtos.DeleteTechnology(); + request.id = id; + return await client.delete(request); +}; + +// ============================================ +// TECH STACKS +// ============================================ + +export const getTechnologyStack = async (slug: string) => { + const request = new dtos.GetTechnologyStack(); + request.slug = slug; + return await client.get(request); +}; + +export const getAllTechnologyStacks = async () => { + return await client.get(new dtos.GetAllTechnologyStacks()); +}; + +export const createTechnologyStack = async (args: any, screenshot?: File) => { + const request = new dtos.CreateTechnologyStack(); + const body = toFormData({ ...args, screenshot }); + return (await client.postBody(request, body)).result; +}; + +export const updateTechnologyStack = async (args: any, screenshot?: File) => { + const request = new dtos.UpdateTechnologyStack(); + const body = toFormData({ ...args, screenshot }); + return (await client.putBody(request, body)).result; +}; + +// ============================================ +// POSTS +// ============================================ + +export const queryPosts = async (query: any) => { + return await client.get( + new dtos.QueryPosts(), + { + take: 50, + ...query, + fields: 'id,organizationId,userId,type,categoryId,slug,title,imageUrl,labels,technologyIds,upVotes,downVotes,points,commentsCount,created,createdBy' + } + ); +}; + +export const getPost = async (id: number) => { + const request = new dtos.GetPost(); + request.id = id; + return await client.get(request); +}; + +export const createPost = async (args: any, image?: File) => { + const request = new dtos.CreatePost(); + const body = toFormData({ ...args, image }); + return (await client.postBody(request, body)).result; +}; + +export const updatePost = async (args: any, image?: File) => { + const request = new dtos.UpdatePost(); + const body = toFormData({ ...args, image }); + return (await client.putBody(request, body)).result; +}; + +export const deletePost = async (id: number) => { + const request = new dtos.DeletePost(); + request.id = id; + return await client.delete(request); +}; + +export const votePost = async (id: number, weight: number) => { + const request = new dtos.UserPostVote(); + request.id = id; + request.weight = weight; + return await client.put(request); +}; + +export const favoritePost = async (id: number) => { + const request = new dtos.UserPostFavorite(); + request.id = id; + return await client.put(request); +}; + +// ============================================ +// COMMENTS +// ============================================ + +export const createPostComment = async (args: any) => { + const request = new dtos.CreatePostComment(); + Object.assign(request, args); + return (await client.post(request)).result; +}; + +export const votePostComment = async (id: number, weight: number) => { + const request = new dtos.UserPostCommentVote(); + request.id = id; + request.weight = weight; + return await client.put(request); +}; + +// ============================================ +// ORGANIZATIONS +// ============================================ + +export const getOrganizationBySlug = async (slug: string) => { + const request = new dtos.GetOrganizationBySlug(); + request.slug = slug; + return await client.get(request); +}; + +export const createOrganization = async (args: any) => { + const request = new dtos.CreateOrganization(); + Object.assign(request, args); + return (await client.post(request)).result; +}; + +// ============================================ +// FAVORITES +// ============================================ + +export const addFavoriteTechnology = async (id: number) => { + const request = new dtos.AddFavoriteTechnology(); + request.technologyId = id; + return (await client.put(request)).result; +}; + +export const removeFavoriteTechnology = async (id: number) => { + const request = new dtos.RemoveFavoriteTechnology(); + request.technologyId = id; + return (await client.delete(request)).result; +}; + +export const addFavoriteTechStack = async (id: number) => { + const request = new dtos.AddFavoriteTechStack(); + request.technologyStackId = id; + return (await client.put(request)).result; +}; + +export const removeFavoriteTechStack = async (id: number) => { + const request = new dtos.RemoveFavoriteTechStack(); + request.technologyStackId = id; + return (await client.delete(request)).result; +}; + +// ============================================ +// USER INFO +// ============================================ + +export const getUserInfo = async (userName: string) => { + const request = new dtos.GetUserInfo(); + request.userName = userName; + return await client.get(request); +}; + +// Error handling helper +export function handleApiError(error: any) { + if (error.responseStatus) { + return { + message: error.responseStatus.message, + errors: error.responseStatus.errors || [] + }; + } + return { message: error.message || 'An error occurred' }; +} +``` + +### 4.3 DTO Integration + +**Copy DTOs: `shared/dtos.ts`** + +```bash +# Copy the generated DTOs from the Nuxt project +cp TechStacks/src/shared/dtos.ts nextjs-app/src/shared/dtos.ts +``` + +**Update DTO Generation Script in C# project:** + +```json +// TechStacks/package.json - Update dtos script +{ + "scripts": { + "dtos": "cd src/shared && x ts && tsc -m ES6 dtos.ts && cp dtos.ts ../../nextjs-app/src/shared/dtos.ts" + } +} +``` + +This ensures DTOs are automatically synced between projects. + +### 4.4 Next.js API Proxy Configuration + +**Configuration: `next.config.ts`** + +```typescript +import type { NextConfig } from 'next'; + +const nextConfig: NextConfig = { + // Rewrite API requests to C# backend during development + async rewrites() { + return [ + { + source: '/api/:path*', + destination: 'https://localhost:5001/api/:path*' + }, + { + source: '/auth/:path*', + destination: 'https://localhost:5001/auth/:path*' + }, + { + source: '/users/:id/avatar', + destination: 'https://localhost:5001/users/:id/avatar' + } + ]; + }, + + // Production build output to C# wwwroot + output: 'export', // Static export + distDir: '../TechStacks/wwwroot', + + // Image optimization configuration + images: { + unoptimized: true // Required for static export + }, + + // Trailing slashes for compatibility + trailingSlash: true +}; + +export default nextConfig; +``` + +--- + +## 5. State Management + +### 5.1 Zustand Store Architecture + +**Why Zustand:** +- Lightweight (1kb vs Redux's 11kb) +- No boilerplate (no actions/reducers) +- TypeScript-first +- Works seamlessly with React 19 +- Easy to test + +### 5.2 Store Structure + +**Main Store: `lib/stores/useAppStore.ts`** + +```typescript +import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; +import * as gateway from '@/lib/api/gateway'; +import type { SessionInfo, Technology, TechnologyStack, Post } from '@/shared/dtos'; + +interface AppState { + // Loading state + loading: boolean; + setLoading: (loading: boolean) => void; + + // Session & Auth + sessionInfo: SessionInfo | null; + setSessionInfo: (session: SessionInfo | null) => void; + isAuthenticated: () => boolean; + isAdmin: () => boolean; + + // Config & Overview + config: any; + overview: any; + setConfig: (config: any) => void; + setOverview: (overview: any) => void; + + // Cached data + technologies: Technology[]; + techStacks: TechnologyStack[]; + posts: Record; + organizations: any[]; + + // User data + favoriteTechnologyIds: number[]; + favoriteTechStackIds: number[]; + userVotes: Record; // postId -> weight + userCommentVotes: Record; // commentId -> weight + + // Actions + initialize: () => Promise; + loadTechnology: (slug: string) => Promise; + loadTechnologyStack: (slug: string) => Promise; + addFavoriteTechnology: (id: number) => Promise; + removeFavoriteTechnology: (id: number) => Promise; + votePost: (id: number, weight: number) => Promise; +} + +export const useAppStore = create()( + persist( + (set, get) => ({ + // Initial state + loading: false, + sessionInfo: null, + config: null, + overview: null, + technologies: [], + techStacks: [], + posts: {}, + organizations: [], + favoriteTechnologyIds: [], + favoriteTechStackIds: [], + userVotes: {}, + userCommentVotes: {}, + + // Setters + setLoading: (loading) => set({ loading }), + setSessionInfo: (sessionInfo) => { + set({ + sessionInfo, + favoriteTechnologyIds: sessionInfo?.favoriteTechnologyIds || [], + favoriteTechStackIds: sessionInfo?.favoriteTechStackIds || [] + }); + }, + setConfig: (config) => set({ config }), + setOverview: (overview) => set({ overview }), + + // Computed + isAuthenticated: () => get().sessionInfo !== null, + isAdmin: () => { + const roles = get().sessionInfo?.roles || []; + return roles.includes('Admin'); + }, + + // Initialize app + initialize: async () => { + set({ loading: true }); + try { + const [config, overview, sessionInfo] = await Promise.all([ + gateway.getConfig(), + gateway.getOverview(), + gateway.getSessionInfo() + ]); + + set({ + config, + overview, + sessionInfo, + favoriteTechnologyIds: sessionInfo?.favoriteTechnologyIds || [], + favoriteTechStackIds: sessionInfo?.favoriteTechStackIds || [], + loading: false + }); + } catch (error) { + console.error('Failed to initialize app:', error); + set({ loading: false }); + } + }, + + // Load technology + loadTechnology: async (slug: string) => { + const tech = await gateway.getTechnology(slug); + set((state) => ({ + technologies: [ + ...state.technologies.filter(t => t.slug !== slug), + tech.result + ] + })); + return tech.result; + }, + + // Load tech stack + loadTechnologyStack: async (slug: string) => { + const stack = await gateway.getTechnologyStack(slug); + set((state) => ({ + techStacks: [ + ...state.techStacks.filter(s => s.slug !== slug), + stack.result + ] + })); + return stack.result; + }, + + // Add favorite technology + addFavoriteTechnology: async (id: number) => { + await gateway.addFavoriteTechnology(id); + set((state) => ({ + favoriteTechnologyIds: [...state.favoriteTechnologyIds, id] + })); + }, + + // Remove favorite technology + removeFavoriteTechnology: async (id: number) => { + await gateway.removeFavoriteTechnology(id); + set((state) => ({ + favoriteTechnologyIds: state.favoriteTechnologyIds.filter(tid => tid !== id) + })); + }, + + // Vote on post + votePost: async (id: number, weight: number) => { + await gateway.votePost(id, weight); + set((state) => ({ + userVotes: { ...state.userVotes, [id]: weight } + })); + } + }), + { + name: 'techstacks-storage', + // Only persist specific keys + partialize: (state) => ({ + sessionInfo: state.sessionInfo, + favoriteTechnologyIds: state.favoriteTechnologyIds, + favoriteTechStackIds: state.favoriteTechStackIds + }) + } + ) +); +``` + +### 5.3 Using the Store in Components + +**Example: Favorite Technology Button** + +```tsx +'use client' + +import { useAppStore } from '@/lib/stores/useAppStore'; + +export function FavoriteButton({ techId }: { techId: number }) { + const { + favoriteTechnologyIds, + addFavoriteTechnology, + removeFavoriteTechnology, + isAuthenticated + } = useAppStore(); + + const isFavorite = favoriteTechnologyIds.includes(techId); + + const toggleFavorite = async () => { + if (!isAuthenticated()) { + // Redirect to login + window.location.href = '/login/github'; + return; + } + + if (isFavorite) { + await removeFavoriteTechnology(techId); + } else { + await addFavoriteTechnology(techId); + } + }; + + return ( + + ); +} +``` + +--- + +## 6. Authentication & Authorization + +### 6.1 Authentication Flow + +**OAuth Flow (GitHub):** + +1. User clicks "Sign in with GitHub" +2. Redirect to `/login/github` +3. C# backend handles OAuth redirect +4. Backend sets authentication cookie +5. Client fetches session info +6. Store session in Zustand + +### 6.2 Auth Provider Component + +**Component: `components/providers/AuthProvider.tsx`** + +```tsx +'use client' + +import { useEffect } from 'react'; +import { useAppStore } from '@/lib/stores/useAppStore'; + +export function AuthProvider({ children }: { children: React.ReactNode }) { + const initialize = useAppStore((state) => state.initialize); + + useEffect(() => { + initialize(); + }, [initialize]); + + return <>{children}; +} +``` + +**Root Layout: `app/layout.tsx`** + +```tsx +import { AuthProvider } from '@/components/providers/AuthProvider'; + +export default function RootLayout({ children }) { + return ( + + + + {children} + + + + ); +} +``` + +### 6.3 Protected Routes + +**Hook: `lib/hooks/useRequireAuth.ts`** + +```typescript +'use client' + +import { useEffect } from 'react'; +import { useRouter } from 'next/navigation'; +import { useAppStore } from '@/lib/stores/useAppStore'; + +export function useRequireAuth() { + const router = useRouter(); + const isAuthenticated = useAppStore((state) => state.isAuthenticated()); + + useEffect(() => { + if (!isAuthenticated) { + router.push('/login/github'); + } + }, [isAuthenticated, router]); + + return isAuthenticated; +} +``` + +**Usage in page:** + +```tsx +'use client' + +import { useRequireAuth } from '@/lib/hooks/useRequireAuth'; + +export default function FavoritesPage() { + const isAuthenticated = useRequireAuth(); + + if (!isAuthenticated) { + return
Redirecting to login...
; + } + + return
Your Favorites
; +} +``` + +### 6.4 Role-Based Access Control + +**Hook: `lib/hooks/useAuthorization.ts`** + +```typescript +import { useAppStore } from '@/lib/stores/useAppStore'; + +export function useAuthorization() { + const { sessionInfo, isAdmin } = useAppStore(); + + const canEditTechnology = (tech: Technology) => { + if (!sessionInfo) return false; + if (isAdmin()) return true; + if (tech.isLocked) return false; + return sessionInfo.userAuthId === tech.ownerId; + }; + + const canEditTechStack = (stack: TechnologyStack) => { + if (!sessionInfo) return false; + if (isAdmin()) return true; + if (stack.isLocked) return false; + return sessionInfo.userAuthId === stack.ownerId; + }; + + const isOrganizationModerator = (org: Organization) => { + if (isAdmin()) return true; + const member = org.members?.find(m => m.userId === sessionInfo?.userId); + return member?.isModerator || false; + }; + + return { + canEditTechnology, + canEditTechStack, + isOrganizationModerator, + isAdmin + }; +} +``` + +--- + +## 7. Routing & Navigation + +### 7.1 Route Mapping (Nuxt → Next.js) + +| Nuxt.js Route | Next.js App Router Route | File Path | +|---------------|-------------------------|-----------| +| `/` | `/` | `app/page.tsx` | +| `/top` | `/top` | `app/top/page.tsx` | +| `/tech` | `/tech` | `app/tech/page.tsx` | +| `/tech/new` | `/tech/new` | `app/tech/new/page.tsx` | +| `/tech/:slug` | `/tech/[slug]` | `app/tech/[slug]/page.tsx` | +| `/tech/:slug/edit` | `/tech/[slug]/edit` | `app/tech/[slug]/edit/page.tsx` | +| `/stacks` | `/stacks` | `app/stacks/page.tsx` | +| `/stacks/new` | `/stacks/new` | `app/stacks/new/page.tsx` | +| `/stacks/:slug` | `/stacks/[slug]` | `app/stacks/[slug]/page.tsx` | +| `/stacks/:slug/edit` | `/stacks/[slug]/edit` | `app/stacks/[slug]/edit/page.tsx` | +| `/organizations` | `/organizations` | `app/organizations/page.tsx` | +| `/organizations/:slug` | `/organizations/[slug]` | `app/organizations/[slug]/page.tsx` | +| `/favorites` | `/favorites` | `app/favorites/page.tsx` | +| `/news` | `/news` | `app/news/page.tsx` | +| `/users/:id` | `/users/[id]` | `app/users/[id]/page.tsx` | +| `/posts/:id/:slug` | `/posts/[id]/[slug]` | `app/posts/[id]/[slug]/page.tsx` | +| `/comments/:postid/:id` | `/comments/[postid]/[id]` | `app/comments/[postid]/[id]/page.tsx` | +| `/login/:provider` | `/login/[provider]` | `app/login/[provider]/page.tsx` | +| `/:slug` | `/[slug]` | `app/[slug]/page.tsx` | +| `/:slug/:category` | `/[slug]/[category]` | `app/[slug]/[category]/page.tsx` | + +### 7.2 Navigation Helpers + +**Utilities: `lib/utils/routes.ts`** + +```typescript +export const routes = { + home: () => '/', + top: () => '/top', + + // Technology routes + tech: (slug?: string) => slug ? `/tech/${slug}` : '/tech', + techNew: () => '/tech/new', + techEdit: (slug: string) => `/tech/${slug}/edit`, + + // Stack routes + stack: (slug?: string) => slug ? `/stacks/${slug}` : '/stacks', + stackNew: () => '/stacks/new', + stackEdit: (slug: string) => `/stacks/${slug}/edit`, + + // Organization routes + organization: (slug?: string) => slug ? `/organizations/${slug}` : '/organizations', + organizationCategory: (slug: string, category: string) => `/${slug}/${category}`, + + // Post routes + post: (id: number, slug: string) => `/posts/${id}/${slug}`, + + // User routes + user: (id: number) => `/users/${id}`, + + // Auth + login: (provider: string = 'github') => `/login/${provider}`, + + // Other + favorites: () => '/favorites', + news: () => '/news' +}; +``` + +**Usage in components:** + +```tsx +import Link from 'next/link'; +import { routes } from '@/lib/utils/routes'; + +export function TechnologyCard({ tech }) { + return ( + + {tech.name} + + ); +} +``` + +### 7.3 Metadata Generation + +**Example: Dynamic Metadata for Technology Page** + +```typescript +// app/tech/[slug]/page.tsx +import { Metadata } from 'next'; +import { gateway } from '@/lib/api/gateway'; + +type Props = { + params: Promise<{ slug: string }>; +}; + +export async function generateMetadata({ params }: Props): Promise { + const { slug } = await params; + const tech = await gateway.getTechnology(slug); + + return { + title: `${tech.result.name} - TechStacks`, + description: tech.result.description, + openGraph: { + title: tech.result.name, + description: tech.result.description, + images: tech.result.logoUrl ? [tech.result.logoUrl] : [] + } + }; +} + +export default async function TechnologyPage({ params }: Props) { + const { slug } = await params; + const tech = await gateway.getTechnology(slug); + + return ( +
+

{tech.result.name}

+ {/* ... */} +
+ ); +} +``` + +--- + +## 8. Page Migration Matrix + +### 8.1 Complete Page Inventory + +| # | Nuxt Page | Next.js Route | Priority | Complexity | Estimated Hours | +|---|-----------|--------------|----------|------------|----------------| +| 1 | `pages/index.vue` | `app/page.tsx` | Critical | High | 8h | +| 2 | `pages/top/index.vue` | `app/top/page.tsx` | High | Medium | 4h | +| 3 | `pages/tech/index.vue` | `app/tech/page.tsx` | Critical | Medium | 6h | +| 4 | `pages/tech/new.vue` | `app/tech/new/page.tsx` | High | Medium | 4h | +| 5 | `pages/tech/_slug/index.vue` | `app/tech/[slug]/page.tsx` | Critical | High | 8h | +| 6 | `pages/tech/_slug/edit.vue` | `app/tech/[slug]/edit/page.tsx` | High | High | 6h | +| 7 | `pages/stacks/index.vue` | `app/stacks/page.tsx` | Critical | Medium | 6h | +| 8 | `pages/stacks/new.vue` | `app/stacks/new/page.tsx` | High | High | 6h | +| 9 | `pages/stacks/_slug/index.vue` | `app/stacks/[slug]/page.tsx` | Critical | High | 8h | +| 10 | `pages/stacks/_slug/edit.vue` | `app/stacks/[slug]/edit/page.tsx` | High | High | 6h | +| 11 | `pages/organizations/index.vue` | `app/organizations/page.tsx` | Medium | Medium | 4h | +| 12 | `pages/organizations/_slug/index.vue` | `app/organizations/[slug]/page.tsx` | High | High | 10h | +| 13 | `pages/favorites/index.vue` | `app/favorites/page.tsx` | Medium | Medium | 4h | +| 14 | `pages/news/index.vue` | `app/news/page.tsx` | Medium | Low | 3h | +| 15 | `pages/users/_id.vue` | `app/users/[id]/page.tsx` | Medium | Medium | 5h | +| 16 | `pages/posts/_id/_postslug.vue` | `app/posts/[id]/[slug]/page.tsx` | High | High | 8h | +| 17 | `pages/comments/_postid/_id.vue` | `app/comments/[postid]/[id]/page.tsx` | Low | Medium | 4h | +| 18 | `pages/login/_provider.vue` | `app/login/[provider]/page.tsx` | Critical | Low | 2h | +| 19 | `pages/_slug/index.vue` | `app/[slug]/page.tsx` | Medium | High | 6h | +| 20 | `pages/_slug/_category.vue` | `app/[slug]/[category]/page.tsx` | Low | Medium | 4h | + +**Total Estimated Hours: 112 hours (~14-16 days for 1 developer)** + +### 8.2 Page Migration Details + +#### **8.2.1 Home Page (`pages/index.vue` → `app/page.tsx`)** + +**Current Functionality:** +- News feed with latest posts +- Technology filtering +- Post type filtering (Announcement, Post, Showcase, etc.) +- Sorting options +- Infinite scroll/pagination +- Keyboard shortcuts (j/k navigation) + +**Migration Strategy:** +```tsx +// app/page.tsx - Server Component +import { NewsFeed } from '@/components/posts/NewsFeed'; +import { gateway } from '@/lib/api/gateway'; + +export default async function HomePage() { + // Initial data fetch (SSR for SEO) + const initialPosts = await gateway.queryPosts({ take: 50 }); + + return ( +
+

Latest News

+ +
+ ); +} + +// components/posts/NewsFeed.tsx - Client Component +'use client' +export function NewsFeed({ initialPosts }) { + const [posts, setPosts] = useState(initialPosts); + const [filters, setFilters] = useState({}); + // ... rest of interactive logic +} +``` + +#### **8.2.2 Technology Detail (`tech/_slug/index.vue` → `tech/[slug]/page.tsx`)** + +**Current Functionality:** +- Technology information display +- Comments section +- Related posts +- Edit/delete buttons (for authorized users) +- Favorite button +- Technology stacks using this tech + +**Migration Strategy:** +```tsx +// app/tech/[slug]/page.tsx +import { Metadata } from 'next'; +import { gateway } from '@/lib/api/gateway'; +import { TechnologyHeader } from '@/components/tech/TechnologyHeader'; +import { TechnologyComments } from '@/components/tech/TechnologyComments'; +import { TechnologyStacks } from '@/components/tech/TechnologyStacks'; + +type Props = { + params: Promise<{ slug: string }>; +}; + +export async function generateMetadata({ params }: Props): Promise { + const { slug } = await params; + const response = await gateway.getTechnology(slug); + const tech = response.result; + + return { + title: `${tech.name} - TechStacks`, + description: tech.description + }; +} + +export default async function TechnologyPage({ params }: Props) { + const { slug } = await params; + const response = await gateway.getTechnology(slug); + const tech = response.result; + + return ( +
+ {/* Server-rendered header for SEO */} + + + {/* Client components for interactivity */} + + +
+ ); +} +``` + +#### **8.2.3 Organization Detail (`organizations/_slug/index.vue` → `organizations/[slug]/page.tsx`)** + +**Current Functionality (most complex page - 10+ sections):** +- Organization info card +- Member list with roles +- Post feed with moderation +- Category management +- Label management +- Post creation form +- Subscribe/unsubscribe +- Moderator-only sections +- Admin-only sections + +**Migration Strategy:** +Break into multiple components: +```tsx +// app/organizations/[slug]/page.tsx +export default async function OrganizationPage({ params }: Props) { + const { slug } = await params; + const org = await gateway.getOrganizationBySlug(slug); + + return ( +
+ {/* Sidebar */} + + + {/* Main content */} +
+ + + {/* Moderator sections - conditionally rendered */} + +
+
+ ); +} +``` + +--- + +## 9. Component Migration Strategy + +### 9.1 Component Inventory + +**23 Vue Components → React Components** + +| Vue Component | React Component | Type | Priority | Hours | +|---------------|----------------|------|----------|-------| +| `TechStackEdit.vue` | `TechStackForm.tsx` | Form | High | 6h | +| `TechnologyEdit.vue` | `TechnologyForm.tsx` | Form | High | 5h | +| `OrganizationEdit.vue` | `OrganizationForm.tsx` | Form | High | 8h | +| `PostEdit.vue` | `PostForm.tsx` | Form | High | 5h | +| `CommentEdit.vue` | `CommentForm.tsx` | Form | Medium | 3h | +| `CategoryEdit.vue` | `CategoryForm.tsx` | Form | Low | 2h | +| `LabelEdit.vue` | `LabelForm.tsx` | Form | Low | 2h | +| `MemberEdit.vue` | `MemberForm.tsx` | Form | Low | 2h | +| `NewsPosts.vue` | `NewsPosts.tsx` | Display | High | 6h | +| `PostsList.vue` | `PostsList.tsx` | Display | High | 4h | +| `PostComments.vue` | `PostComments.tsx` | Display | High | 5h | +| `PostComment.vue` | `PostComment.tsx` | Display | Medium | 3h | +| `TechnologyPost.vue` | `TechnologyPost.tsx` | Display | Medium | 3h | +| `TechnologyComments.vue` | `TechnologyComments.tsx` | Display | Medium | 4h | +| `OrganizationInfo.vue` | `OrganizationInfo.tsx` | Display | Medium | 3h | +| `MembersInfo.vue` | `MembersInfo.tsx` | Display | Low | 2h | +| `PostInfo.vue` | `PostInfo.tsx` | Display | Low | 2h | +| `PostAlerts.vue` | `PostAlerts.tsx` | Display | Low | 2h | +| `Shortcuts.vue` | `Shortcuts.tsx` | Dialog | Low | 2h | +| `ReportDialog.vue` | `ReportDialog.tsx` | Dialog | Low | 2h | +| `FileInput.vue` | `FileInput.tsx` | Input | Medium | 2h | +| `DebugInfo.vue` | `DebugInfo.tsx` | Utility | Low | 1h | + +**Total: ~74 hours** + +### 9.2 Vue to React Conversion Patterns + +#### **9.2.1 Template → JSX** + +**Vue:** +```vue + +``` + +**React:** +```tsx +export function TechnologyCard({ technology }) { + if (!technology) return null; + + return ( +
+

{technology.name}

+ {technology.description &&

{technology.description}

} + {canEdit && } +
+ ); +} +``` + +#### **9.2.2 Props & Emits → Props & Callbacks** + +**Vue:** +```vue + +``` + +**React:** +```tsx +interface PostCardProps { + post: Post; + editable?: boolean; + onUpdate?: (id: number) => void; + onDelete?: (id: number) => void; +} + +export function PostCard({ + post, + editable = false, + onUpdate, + onDelete +}: PostCardProps) { + const handleUpdate = () => { + onUpdate?.(post.id); + }; + + return (/* ... */); +} +``` + +#### **9.2.3 Data & Computed → useState & useMemo** + +**Vue:** +```vue + +``` + +**React:** +```tsx +export function MyComponent() { + const [count, setCount] = useState(0); + const [items, setItems] = useState([]); + + const total = useMemo(() => items.length, [items]); + const isEmpty = useMemo(() => total === 0, [total]); + + return (/* ... */); +} +``` + +#### **9.2.4 Watch → useEffect** + +**Vue:** +```vue + +``` + +**React:** +```tsx +useEffect(() => { + loadData(slug); +}, [slug]); +``` + +#### **9.2.5 Lifecycle Hooks** + +**Vue:** +```vue + +``` + +**React:** +```tsx +useEffect(() => { + initialize(); + + return () => { + cleanup(); + }; +}, []); // Empty array = mount/unmount only +``` + +### 9.3 Form Component Example + +**Vue: `TechnologyEdit.vue`** +```vue + + + +``` + +**React: `TechnologyForm.tsx`** +```tsx +'use client' + +import { useState } from 'react'; +import { useRouter } from 'next/navigation'; +import * as gateway from '@/lib/api/gateway'; +import { TechnologyTier } from '@/shared/dtos'; + +interface TechnologyFormProps { + technology?: Technology; +} + +export function TechnologyForm({ technology }: TechnologyFormProps) { + const router = useRouter(); + const [formData, setFormData] = useState({ + name: technology?.name || '', + tier: technology?.tier || TechnologyTier.ProgrammingLanguage, + description: technology?.description || '' + }); + const [errors, setErrors] = useState>({}); + const [loading, setLoading] = useState(false); + + const validateForm = () => { + const newErrors: Record = {}; + + if (!formData.name.trim()) { + newErrors.name = 'Name is required'; + } + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!validateForm()) return; + + setLoading(true); + try { + if (technology) { + await gateway.updateTechnology({ id: technology.id, ...formData }); + } else { + await gateway.createTechnology(formData); + } + router.push('/tech'); + } catch (error) { + const apiErrors = gateway.handleApiError(error); + setErrors({ submit: apiErrors.message }); + } finally { + setLoading(false); + } + }; + + return ( +
+
+ + setFormData({ ...formData, name: e.target.value })} + className="mt-1 block w-full rounded-md border-gray-300" + required + /> + {errors.name && ( +

{errors.name}

+ )} +
+ +
+ + +
+ + + + {errors.submit && ( +

{errors.submit}

+ )} +
+ ); +} +``` + +### 9.4 Reusable UI Component Library + +**Build with Radix UI + Tailwind (shadcn/ui pattern):** + +Create base components: +- `Button.tsx` +- `Input.tsx` +- `Select.tsx` +- `Checkbox.tsx` +- `Dialog.tsx` +- `DropdownMenu.tsx` +- `Card.tsx` +- `Badge.tsx` +- `Avatar.tsx` + +**Example: `components/ui/Button.tsx`** +```tsx +import { forwardRef } from 'react'; +import { cva, type VariantProps } from 'class-variance-authority'; +import { cn } from '@/lib/utils'; + +const buttonVariants = cva( + 'inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 disabled:opacity-50 disabled:pointer-events-none', + { + variants: { + variant: { + default: 'bg-blue-600 text-white hover:bg-blue-700', + destructive: 'bg-red-600 text-white hover:bg-red-700', + outline: 'border border-gray-300 hover:bg-gray-100', + ghost: 'hover:bg-gray-100', + link: 'underline-offset-4 hover:underline text-blue-600' + }, + size: { + default: 'h-10 py-2 px-4', + sm: 'h-9 px-3 text-sm', + lg: 'h-11 px-8', + icon: 'h-10 w-10' + } + }, + defaultVariants: { + variant: 'default', + size: 'default' + } + } +); + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps {} + +const Button = forwardRef( + ({ className, variant, size, ...props }, ref) => { + return ( +