Some checks failed
Build and Deploy to k3s / build-and-deploy (push) Failing after 1m31s
Complete framework migration from Angular to Next.js with full feature parity. ## What Changed - Migrated from Angular 20 to Next.js 15 with App Router - Replaced Angular components with React functional components - Implemented React Context API for state management (replacing RxJS) - Integrated React Hook Form for form handling - Added shadcn/ui component library - Configured Tailwind CSS 4.1 with @tailwindcss/postcss - Implemented JWT authentication with middleware protection ## Core Features Implemented - ✅ User registration and login with validation - ✅ JWT token authentication with localStorage - ✅ Protected dashboard route with middleware - ✅ Portfolio listing with status indicators - ✅ Create portfolio functionality - ✅ ZIP file upload with progress tracking - ✅ Portfolio deployment - ✅ Responsive design with Tailwind CSS ## Technical Stack - Framework: Next.js 15 (App Router) - Language: TypeScript 5.8 - Styling: Tailwind CSS 4.1 - UI Components: shadcn/ui + Lucide icons - State: React Context API + hooks - Forms: React Hook Form - API Client: Native fetch with custom wrapper ## File Structure - /app - Next.js pages (landing, login, register, dashboard) - /components - React components (ui primitives, auth provider) - /lib - API client, types, utilities - /hooks - Custom hooks (useAuth, usePortfolios) - /middleware.ts - Route protection - /angular-backup - Preserved Angular source code ## API Compatibility - All backend endpoints remain unchanged - JWT Bearer token authentication preserved - API response format maintained ## Build Output - Production build: 7 routes compiled successfully - Bundle size: ~102kB shared JS (optimized) - First Load JS: 103-126kB per route ## Documentation - Updated README.md with Next.js setup guide - Created OpenSpec proposal in openspec/changes/migrate-to-nextjs-launchui/ - Added project context in openspec/project.md 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
111 lines
3.1 KiB
TypeScript
111 lines
3.1 KiB
TypeScript
'use client';
|
|
|
|
import React, { createContext, useState, useEffect, ReactNode } from 'react';
|
|
import { useRouter } from 'next/navigation';
|
|
import { apiClient } from '@/lib/api-client';
|
|
import { User, LoginFormData, RegisterFormData, LoginResponse, RegisterResponse } from '@/lib/types';
|
|
|
|
interface AuthContextType {
|
|
user: User | null;
|
|
token: string | null;
|
|
isAuthenticated: boolean;
|
|
isLoading: boolean;
|
|
login: (credentials: LoginFormData) => Promise<void>;
|
|
register: (data: RegisterFormData) => Promise<void>;
|
|
logout: () => void;
|
|
}
|
|
|
|
export const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
|
|
|
export function AuthProvider({ children }: { children: ReactNode }) {
|
|
const [user, setUser] = useState<User | null>(null);
|
|
const [token, setToken] = useState<string | null>(null);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const router = useRouter();
|
|
|
|
useEffect(() => {
|
|
// Check for existing token on mount
|
|
const storedToken = localStorage.getItem('auth_token');
|
|
if (storedToken) {
|
|
setToken(storedToken);
|
|
// Optionally fetch user data
|
|
fetchUser(storedToken);
|
|
} else {
|
|
setIsLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
const fetchUser = async (authToken: string) => {
|
|
try {
|
|
const response = await apiClient.get<User>('/auth/user');
|
|
if (response.success && response.data) {
|
|
setUser(response.data);
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to fetch user:', error);
|
|
// Token might be invalid
|
|
localStorage.removeItem('auth_token');
|
|
setToken(null);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
const login = async (credentials: LoginFormData) => {
|
|
try {
|
|
const response = await apiClient.post<LoginResponse>('/auth/login', credentials);
|
|
|
|
if (response.success && response.data) {
|
|
const { token: authToken, user: userData } = response.data;
|
|
localStorage.setItem('auth_token', authToken);
|
|
setToken(authToken);
|
|
setUser(userData);
|
|
router.push('/dashboard');
|
|
} else {
|
|
throw new Error(response.message || 'Login failed');
|
|
}
|
|
} catch (error) {
|
|
console.error('Login error:', error);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
const register = async (data: RegisterFormData) => {
|
|
try {
|
|
const response = await apiClient.post<RegisterResponse>('/auth/register', data);
|
|
|
|
if (response.success && response.data) {
|
|
const { token: authToken, user: userData } = response.data;
|
|
localStorage.setItem('auth_token', authToken);
|
|
setToken(authToken);
|
|
setUser(userData);
|
|
router.push('/dashboard');
|
|
} else {
|
|
throw new Error(response.message || 'Registration failed');
|
|
}
|
|
} catch (error) {
|
|
console.error('Registration error:', error);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
const logout = () => {
|
|
localStorage.removeItem('auth_token');
|
|
setToken(null);
|
|
setUser(null);
|
|
router.push('/login');
|
|
};
|
|
|
|
const value: AuthContextType = {
|
|
user,
|
|
token,
|
|
isAuthenticated: !!token,
|
|
isLoading,
|
|
login,
|
|
register,
|
|
logout,
|
|
};
|
|
|
|
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
|
}
|