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>
110 lines
2.8 KiB
TypeScript
110 lines
2.8 KiB
TypeScript
import { ApiResponse } from './types';
|
|
|
|
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000/api';
|
|
|
|
class ApiClient {
|
|
private getToken(): string | null {
|
|
if (typeof window === 'undefined') return null;
|
|
return localStorage.getItem('auth_token');
|
|
}
|
|
|
|
private async request<T>(
|
|
endpoint: string,
|
|
options: RequestInit = {}
|
|
): Promise<ApiResponse<T>> {
|
|
const token = this.getToken();
|
|
const headers: Record<string, string> = {
|
|
'Content-Type': 'application/json',
|
|
...(options.headers as Record<string, string>),
|
|
};
|
|
|
|
if (token) {
|
|
headers['Authorization'] = `Bearer ${token}`;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
|
|
...options,
|
|
headers,
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (!response.ok) {
|
|
if (response.status === 401) {
|
|
// Unauthorized - clear token and redirect to login
|
|
if (typeof window !== 'undefined') {
|
|
localStorage.removeItem('auth_token');
|
|
window.location.href = '/login';
|
|
}
|
|
}
|
|
throw new Error(data.message || 'API request failed');
|
|
}
|
|
|
|
return data;
|
|
} catch (error) {
|
|
console.error('API Error:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async get<T>(endpoint: string): Promise<ApiResponse<T>> {
|
|
return this.request<T>(endpoint, { method: 'GET' });
|
|
}
|
|
|
|
async post<T>(endpoint: string, body?: any): Promise<ApiResponse<T>> {
|
|
return this.request<T>(endpoint, {
|
|
method: 'POST',
|
|
body: body ? JSON.stringify(body) : undefined,
|
|
});
|
|
}
|
|
|
|
async put<T>(endpoint: string, body?: any): Promise<ApiResponse<T>> {
|
|
return this.request<T>(endpoint, {
|
|
method: 'PUT',
|
|
body: body ? JSON.stringify(body) : undefined,
|
|
});
|
|
}
|
|
|
|
async delete<T>(endpoint: string): Promise<ApiResponse<T>> {
|
|
return this.request<T>(endpoint, { method: 'DELETE' });
|
|
}
|
|
|
|
// File upload with multipart/form-data
|
|
async upload<T>(endpoint: string, formData: FormData): Promise<ApiResponse<T>> {
|
|
const token = this.getToken();
|
|
const headers: Record<string, string> = {};
|
|
|
|
if (token) {
|
|
headers['Authorization'] = `Bearer ${token}`;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
|
|
method: 'POST',
|
|
headers,
|
|
body: formData,
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (!response.ok) {
|
|
if (response.status === 401) {
|
|
if (typeof window !== 'undefined') {
|
|
localStorage.removeItem('auth_token');
|
|
window.location.href = '/login';
|
|
}
|
|
}
|
|
throw new Error(data.message || 'Upload failed');
|
|
}
|
|
|
|
return data;
|
|
} catch (error) {
|
|
console.error('Upload Error:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
|
|
export const apiClient = new ApiClient();
|