Changes done in user-auth and template

This commit is contained in:
tejas.prakash 2025-08-20 12:42:57 +05:30
parent 5ffce5392a
commit 722f878243
27 changed files with 2726 additions and 272 deletions

231
AUTH_IMPLEMENTATION.md Normal file
View File

@ -0,0 +1,231 @@
# Authentication Implementation
## Overview
This document describes the authentication system implementation with proper error handling, separate signup/signin routes, and email verification flow.
## Architecture
### Backend (User Auth Service)
- **Port**: 8011
- **Base URL**: `http://localhost:8011`
- **Database**: PostgreSQL
- **Features**: JWT authentication, email verification, session management
### Frontend (Next.js App)
- **Port**: 3001
- **Base URL**: `http://localhost:3001`
- **Framework**: Next.js 15.4.6 with TypeScript
- **UI**: Tailwind CSS + shadcn/ui components
## Routes Structure
### Frontend Routes
```
/signup - User registration page
/signin - User login page
/auth - Redirects to /signin (legacy support)
/verify-email - Email verification page (handled by backend redirect)
```
### Backend API Endpoints
```
POST /api/auth/register - User registration
POST /api/auth/login - User login
GET /api/auth/verify-email - Email verification (redirects to frontend)
POST /api/auth/logout - User logout
POST /api/auth/refresh - Token refresh
GET /api/auth/me - Get user profile
```
## User Flow
### 1. Registration Flow
1. User visits `/signup`
2. Fills out registration form
3. Submits form → `POST /api/auth/register`
4. Backend creates user account and sends verification email
5. Frontend shows success message and redirects to `/signin` after 3 seconds
6. User receives email with verification link
### 2. Email Verification Flow
1. User clicks verification link in email
2. Link points to: `http://localhost:8011/api/auth/verify-email?token=<token>`
3. Backend verifies token and redirects to: `http://localhost:3001/signin?verified=true`
4. Frontend displays success message: "Email verified successfully! You can now sign in to your account."
### 3. Login Flow
1. User visits `/signin`
2. Fills out login form
3. Submits form → `POST /api/auth/login`
4. Backend validates credentials and returns JWT tokens
5. Frontend stores tokens and redirects to dashboard (`/`)
## Error Handling
### Backend Error Responses
All API endpoints return consistent error format:
```json
{
"success": false,
"error": "Error type",
"message": "Detailed error message"
}
```
### Frontend Error Handling
- **Network Errors**: Display user-friendly messages
- **API Errors**: Show specific error messages from backend
- **Validation Errors**: Client-side validation with immediate feedback
- **Authentication Errors**: Clear messaging for login/registration issues
### Common Error Scenarios
#### Registration Errors
- **Email already exists**: "An account with this email already exists"
- **Username taken**: "Username is already taken"
- **Invalid email format**: "Please enter a valid email address"
- **Weak password**: "Password must be at least 8 characters long"
- **Missing fields**: "Please fill in all required fields"
#### Login Errors
- **Invalid credentials**: "Invalid email or password"
- **Email not verified**: "Please verify your email before signing in"
- **Account locked**: "Account is temporarily locked due to multiple failed attempts"
#### Email Verification Errors
- **Invalid token**: "Verification link is invalid or has expired"
- **Already verified**: "Email is already verified"
- **Token expired**: "Verification link has expired. Please request a new one"
## Components Structure
### Signup Flow
```
/signup
├── SignUpPage (main container)
├── SignUpForm (form component)
└── Success State (after registration)
```
### Signin Flow
```
/signin
├── SignInPage (main container)
├── SignInForm (form component)
└── Verification Messages (from URL params)
```
## API Integration
### Authentication Handler (`authenticationHandler.tsx`)
- Handles API calls to backend
- Proper error propagation
- TypeScript interfaces for type safety
### Key Functions
```typescript
registerUser(data: RegisterData): Promise<ApiResponse>
loginUser(email: string, password: string): Promise<ApiResponse>
```
## Security Features
### Backend Security
- JWT token authentication
- Password hashing (bcrypt)
- Rate limiting on auth endpoints
- CORS configuration
- Helmet security headers
- Session management
### Frontend Security
- Token storage in localStorage
- Automatic token refresh
- Secure API communication
- Input validation and sanitization
## Environment Configuration
### Backend (.env)
```env
PORT=8011
FRONTEND_URL=http://localhost:3001
POSTGRES_HOST=localhost
POSTGRES_DB=user_auth
JWT_SECRET=your-secret-key
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=your-email@gmail.com
SMTP_PASS=your-app-password
```
### Frontend (.env.local)
```env
NEXT_PUBLIC_API_URL=http://localhost:8011
NEXT_PUBLIC_FRONTEND_URL=http://localhost:3001
```
## Testing the Implementation
### 1. Start Services
```bash
# Backend
cd automated-dev-pipeline
docker-compose up user-auth
# Frontend
cd codenuk-frontend-dark-theme
npm run dev
```
### 2. Test Registration
1. Visit `http://localhost:3001/signup`
2. Fill out registration form
3. Submit and check for success message
4. Check email for verification link
### 3. Test Email Verification
1. Click verification link in email
2. Should redirect to `http://localhost:3001/signin?verified=true`
3. Verify success message appears
### 4. Test Login
1. Visit `http://localhost:3001/signin`
2. Enter credentials
3. Should redirect to dashboard on success
## Troubleshooting
### Common Issues
1. **CORS Errors**
- Check backend CORS configuration
- Verify frontend URL in allowed origins
2. **Email Not Sending**
- Check SMTP configuration in backend
- Verify email credentials
3. **Verification Link Not Working**
- Check frontend URL in backend configuration
- Verify token expiration settings
4. **Login Fails After Verification**
- Check if user is properly verified in database
- Verify JWT token generation
### Debug Steps
1. Check browser network tab for API calls
2. Check backend logs for errors
3. Verify database connections
4. Test API endpoints directly with Postman/curl
## Future Enhancements
1. **Password Reset Flow**
2. **Two-Factor Authentication**
3. **Social Login Integration**
4. **Account Lockout Protection**
5. **Session Management Dashboard**
6. **Audit Logging**

118
DYNAMIC_TEMPLATES.md Normal file
View File

@ -0,0 +1,118 @@
# Dynamic Templates Implementation
## Overview
The frontend now fetches templates dynamically from the database instead of using static templates. This allows for real-time template management and custom template creation.
## Changes Made
### 1. Template Service (`src/lib/template-service.ts`)
- Created service to communicate with the template-manager API
- Handles fetching templates by category, individual templates, and creating new templates
- Includes TypeScript interfaces for type safety
### 2. Custom Hook (`src/hooks/useTemplates.ts`)
- Manages template data fetching and state
- Converts database templates to UI format
- Handles loading states and error handling
- Provides template feature fetching
### 3. Custom Template Form (`src/components/custom-template-form.tsx`)
- Form component for creating new templates
- Includes all required fields: type, title, description, category
- Optional fields: icon, gradient, border, text, subtext
- Validates required fields before submission
### 4. Updated Main Dashboard (`src/components/main-dashboard.tsx`)
- Replaced static templates with dynamic database templates
- Added loading and error states
- Dynamic category generation based on available templates
- Custom template creation functionality
- Fallback to static templates if database is unavailable
### 5. UI Components
- Added `textarea.tsx` component for form inputs
- Enhanced existing components with proper styling
## API Integration
### Template Manager Service
- **Base URL**: `http://localhost:8009`
- **Endpoints**:
- `GET /api/templates` - Get all templates grouped by category
- `GET /api/templates/:id` - Get specific template with features
- `GET /api/templates/type/:type` - Get template by type
- `POST /api/templates` - Create new template
### Database Schema
Templates are stored in PostgreSQL with the following structure:
```sql
CREATE TABLE templates (
id UUID PRIMARY KEY,
type VARCHAR(100) UNIQUE,
title VARCHAR(200),
description TEXT,
category VARCHAR(100),
icon VARCHAR(50),
gradient VARCHAR(100),
border VARCHAR(100),
text VARCHAR(100),
subtext VARCHAR(100),
is_active BOOLEAN,
created_at TIMESTAMP,
updated_at TIMESTAMP
);
```
## Usage
### Viewing Templates
1. Templates are automatically loaded from the database on page load
2. If database is unavailable, fallback static templates are shown
3. Templates are grouped by category dynamically
### Creating Custom Templates
1. Click "Create Custom Template" button
2. Fill in required fields (type, title, description, category)
3. Optionally add styling fields (icon, gradient, border, text, subtext)
4. Submit to create the template in the database
5. Template will appear in the list after creation
### Template Features
- Each template can have associated features stored in `template_features` table
- Features are fetched when a template is selected
- Features include complexity, type, and usage statistics
## Error Handling
- Network errors show retry button
- Loading states with spinner
- Graceful fallback to static templates
- Form validation for required fields
## ✅ Implemented Features
### Template Management
- ✅ **Dynamic Template Display** - Templates fetched from database
- ✅ **Custom Template Creation** - Create new templates via form
- ✅ **Template Editing** - Edit existing templates
- ✅ **Template Deletion** - Delete templates with confirmation
- ✅ **Real-time Updates** - Changes reflect immediately in UI
### API Endpoints
- ✅ `GET /api/templates` - Get all templates grouped by category
- ✅ `GET /api/templates/:id` - Get specific template with features
- ✅ `POST /api/templates` - Create new template
- ✅ `PUT /api/templates/:id` - Update existing template
- ✅ `DELETE /api/templates/:id` - Delete template (soft delete)
### Database Operations
- ✅ **Soft Delete** - Templates are marked as inactive rather than physically deleted
- ✅ **Data Integrity** - All operations maintain referential integrity
- ✅ **Error Handling** - Comprehensive error handling for all operations
## Future Enhancements
- Feature management for templates
- Bulk template operations
- Template versioning
- Template sharing between users
- Template import/export functionality
- Template analytics and usage tracking

116
package-lock.json generated
View File

@ -18,6 +18,7 @@
"@radix-ui/react-select": "^2.2.5", "@radix-ui/react-select": "^2.2.5",
"@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-tabs": "^1.1.12", "@radix-ui/react-tabs": "^1.1.12",
"axios": "^1.11.0",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"lucide-react": "^0.539.0", "lucide-react": "^0.539.0",
@ -3012,6 +3013,12 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/available-typed-arrays": { "node_modules/available-typed-arrays": {
"version": "1.0.7", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
@ -3038,6 +3045,17 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/axios": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz",
"integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/axobject-query": { "node_modules/axobject-query": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
@ -3102,7 +3120,6 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"es-errors": "^1.3.0", "es-errors": "^1.3.0",
@ -3258,6 +3275,18 @@
"simple-swizzle": "^0.2.2" "simple-swizzle": "^0.2.2"
} }
}, },
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/concat-map": { "node_modules/concat-map": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@ -3409,6 +3438,15 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/detect-libc": { "node_modules/detect-libc": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
@ -3442,7 +3480,6 @@
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"call-bind-apply-helpers": "^1.0.1", "call-bind-apply-helpers": "^1.0.1",
@ -3547,7 +3584,6 @@
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@ -3557,7 +3593,6 @@
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@ -3595,7 +3630,6 @@
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"es-errors": "^1.3.0" "es-errors": "^1.3.0"
@ -3608,7 +3642,6 @@
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"es-errors": "^1.3.0", "es-errors": "^1.3.0",
@ -4215,6 +4248,26 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/for-each": { "node_modules/for-each": {
"version": "0.3.5", "version": "0.3.5",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
@ -4231,11 +4284,26 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/form-data": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/function-bind": { "node_modules/function-bind": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
@ -4276,7 +4344,6 @@
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"call-bind-apply-helpers": "^1.0.2", "call-bind-apply-helpers": "^1.0.2",
@ -4310,7 +4377,6 @@
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"dunder-proto": "^1.0.1", "dunder-proto": "^1.0.1",
@ -4398,7 +4464,6 @@
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@ -4477,7 +4542,6 @@
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@ -4490,7 +4554,6 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"has-symbols": "^1.0.3" "has-symbols": "^1.0.3"
@ -4506,7 +4569,6 @@
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"function-bind": "^1.1.2" "function-bind": "^1.1.2"
@ -5429,7 +5491,6 @@
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@ -5459,6 +5520,27 @@
"node": ">=8.6" "node": ">=8.6"
} }
}, },
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/minimatch": { "node_modules/minimatch": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@ -5960,6 +6042,12 @@
"react-is": "^16.13.1" "react-is": "^16.13.1"
} }
}, },
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/punycode": { "node_modules/punycode": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",

View File

@ -19,6 +19,7 @@
"@radix-ui/react-select": "^2.2.5", "@radix-ui/react-select": "^2.2.5",
"@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-tabs": "^1.1.12", "@radix-ui/react-tabs": "^1.1.12",
"axios": "^1.11.0",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"lucide-react": "^0.539.0", "lucide-react": "^0.539.0",

View File

@ -0,0 +1,170 @@
"use client";
import React, { useEffect, useRef, useState } from "react";
import { useSearchParams, useRouter } from "next/navigation";
type VerifyState = "idle" | "loading" | "success" | "error";
interface VerificationResponse {
success: boolean;
data: { message: string; user: { email: string; username: string } };
message: string;
}
interface ErrorResponse {
success: false;
error: string;
message: string;
}
const API_BASE_URL = "http://localhost:8011";
const EmailVerification: React.FC = () => {
const searchParams = useSearchParams();
const router = useRouter();
const [status, setStatus] = useState<VerifyState>("idle");
const [message, setMessage] = useState("");
const [error, setError] = useState("");
const token = searchParams.get("token");
const didRun = useRef(false);
useEffect(() => {
if (!token) {
setStatus("error");
setError("No verification token found.");
return;
}
if (didRun.current) return;
didRun.current = true;
void verifyEmail(token);
}, [token]);
const verifyEmail = async (verificationToken: string) => {
setStatus("loading");
setMessage("Verifying your email...");
setError("");
const ctrl = new AbortController();
const timeout = setTimeout(() => ctrl.abort(), 10000);
try {
const res = await fetch(
`${API_BASE_URL}/api/auth/verify-email?token=${verificationToken}`,
{ method: "GET", headers: { "Content-Type": "application/json" }, signal: ctrl.signal }
);
const txt = await res.text();
let data: any = {};
try { data = JSON.parse(txt || "{}"); } catch { /* ignore non-JSON */ }
if (res.ok && (data as VerificationResponse)?.success) {
router.replace("/auth?verified=1");
return;
}
const msg = String(data?.message || "").toLowerCase();
if (msg.includes("already verified")) {
router.replace("/auth?verified=1");
return;
}
setStatus("error");
setError(data?.message || `Verification failed (HTTP ${res.status}).`);
} catch (e: any) {
setStatus("error");
setError(e?.name === "AbortError" ? "Request timed out. Please try again." : "Network error. Please try again.");
console.error("Email verification error:", e);
} finally {
clearTimeout(timeout);
}
};
const handleResendVerification = async () => {
setStatus("loading");
setMessage("Sending verification email...");
setError("");
try {
const email = prompt("Enter your email to resend the verification link:");
if (!email) {
setStatus("error");
setError("Email is required to resend verification.");
return;
}
const res = await fetch(`${API_BASE_URL}/api/auth/resend-verification`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email }),
});
const data = await res.json();
if (res.ok && data?.success) {
setStatus("success");
setMessage("Verification email sent! Please check your inbox.");
} else {
setStatus("error");
setError(data?.message || "Failed to resend verification email.");
}
} catch (e) {
setStatus("error");
setError("Network error. Please try again.");
console.error("Resend verification error:", e);
}
};
if (status === "loading") {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4" />
<h2 className="text-xl font-semibold text-gray-900 mb-1">Verifying Email</h2>
<p className="text-gray-600">{message}</p>
</div>
</div>
);
}
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4">
<div className="max-w-md w-full space-y-6">
{status === "success" && (
<div className="bg-green-50 border border-green-200 rounded-md p-4 text-center">
<p className="text-green-800 font-medium">{message || "Email verified. Redirecting..."}</p>
</div>
)}
{status === "error" && (
<div className="bg-red-50 border border-red-200 rounded-md p-4">
<p className="text-red-800 font-medium text-center">{error}</p>
<button
onClick={handleResendVerification}
className="mt-4 w-full py-2 rounded-md text-white bg-blue-600 hover:bg-blue-700"
>
Resend Verification Email
</button>
</div>
)}
<div className="flex gap-3">
<button
onClick={() => (window.location.href = "/auth")}
className="w-1/2 py-2 rounded-md border bg-white text-gray-700 hover:bg-gray-50"
>
Go to Login
</button>
<button
onClick={() => (window.location.href = "/")}
className="w-1/2 py-2 rounded-md border bg-white text-gray-700 hover:bg-gray-50"
>
Go to Home
</button>
</div>
</div>
</div>
);
};
export default EmailVerification;

View File

@ -1,5 +1,33 @@
import { AuthPage } from "@/components/auth/auth-page" "use client"
import { useEffect } from "react"
import { useRouter, useSearchParams } from "next/navigation"
export default function AuthPageRoute() { export default function AuthPageRoute() {
return <AuthPage /> const router = useRouter()
const searchParams = useSearchParams()
useEffect(() => {
// Check if user wants to sign up or sign in
const mode = searchParams.get('mode')
if (mode === 'signup') {
router.replace('/signup')
} else if (mode === 'signin') {
router.replace('/signin')
} else {
// Default to signin page
router.replace('/signin')
}
}, [router, searchParams])
// Show loading while redirecting
return (
<div className="min-h-screen bg-black text-white flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-orange-500 mx-auto mb-4"></div>
<p className="text-white/60">Redirecting...</p>
</div>
</div>
)
} }

5
src/app/signin/page.tsx Normal file
View File

@ -0,0 +1,5 @@
import { SignInPage } from "@/components/auth/signin-page"
export default function SignInPageRoute() {
return <SignInPage />
}

5
src/app/signup/page.tsx Normal file
View File

@ -0,0 +1,5 @@
import { SignUpPage } from "@/components/auth/signup-page"
export default function SignUpPageRoute() {
return <SignUpPage />
}

View File

@ -0,0 +1,5 @@
import EmailVerification from "@/app/auth/emailVerification";
export default function VerifyEmailPage() {
return <EmailVerification />;
}

View File

@ -0,0 +1,96 @@
import axios from "axios";
import { safeLocalStorage, safeRedirect } from "@/lib/utils";
const API_BASE_URL = "http://localhost:8011";
let accessToken = safeLocalStorage.getItem('accessToken');
let refreshToken = safeLocalStorage.getItem('refreshToken');
export const setTokens = (newAccessToken: string, newRefreshToken: string) => {
accessToken = newAccessToken;
refreshToken = newRefreshToken;
safeLocalStorage.setItem('accessToken', newAccessToken);
safeLocalStorage.setItem('refreshToken', newRefreshToken);
};
export const clearTokens = () => {
accessToken = null;
refreshToken = null;
safeLocalStorage.removeItem('accessToken');
safeLocalStorage.removeItem('refreshToken');
};
export const getAccessToken = () => accessToken;
export const getRefreshToken = () => refreshToken;
// Logout function that calls the backend API
export const logout = async () => {
try {
const refreshToken = getRefreshToken();
if (refreshToken) {
await authApiClient.post('/api/auth/logout', { refreshToken });
}
} catch (error) {
console.error('Logout API call failed:', error);
// Continue with logout even if API call fails
} finally {
// Always clear tokens and redirect
clearTokens();
// Clear any other user data
safeLocalStorage.removeItem('codenuk_user');
// Redirect to signin page
safeRedirect('/signin');
}
};
export const authApiClient = axios.create({
baseURL: API_BASE_URL,
withCredentials: true,
});
// Add auth token to requests
const addAuthTokenInterceptor = (client: typeof authApiClient) => {
client.interceptors.request.use(
(config) => {
if (accessToken) {
config.headers = config.headers || {};
config.headers.Authorization = `Bearer ${accessToken}`;
}
return config;
},
(error) => Promise.reject(error)
);
};
// Handle token refresh
const addTokenRefreshInterceptor = (client: typeof authApiClient) => {
client.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
if (refreshToken) {
const response = await client.post('/api/auth/refresh', {
refreshToken: refreshToken
});
const { accessToken: newAccessToken, refreshToken: newRefreshToken } = response.data.data.tokens;
setTokens(newAccessToken, newRefreshToken);
originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
return client(originalRequest);
}
} catch (refreshError) {
console.error('Token refresh failed:', refreshError);
clearTokens();
window.location.href = '/auth';
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
}
);
};
addAuthTokenInterceptor(authApiClient);
addTokenRefreshInterceptor(authApiClient);

View File

@ -0,0 +1,75 @@
import { authApiClient } from "./authApiClients";
import { safeLocalStorage } from "@/lib/utils";
interface ApiError extends Error {
response?: any;
}
export const registerUser = async (
data: {
username: string;
email: string;
password: string;
first_name: string;
last_name: string;
role: string;
}
) => {
console.log("Registering user with data:", data);
try {
const response = await authApiClient.post(
"/api/auth/register",
data
);
return response.data;
} catch (error: any) {
console.error("Error registering user:", error.response?.data || error.message);
// Create a proper error object that preserves the original response
const enhancedError: ApiError = new Error(
error.response?.data?.message || error.response?.data?.error || "Failed to register user. Please try again."
);
enhancedError.response = error.response;
throw enhancedError;
}
};
export const loginUser = async (email: string, password: string) => {
console.log("Logging in user with email:", email);
try {
const response = await authApiClient.post(
"/api/auth/login",
{ email, password }
);
return response.data;
} catch (error: any) {
console.error("Error logging in user:", error.response?.data || error.message);
// Create a proper error object that preserves the original response
const enhancedError: ApiError = new Error(
error.response?.data?.message || error.response?.data?.error || "Failed to log in. Please check your credentials."
);
enhancedError.response = error.response;
throw enhancedError;
}
}
export const logoutUser = async () => {
console.log("Logging out user");
try {
const refreshToken = safeLocalStorage.getItem('refreshToken');
if (refreshToken) {
await authApiClient.post("/api/auth/logout", { refreshToken });
}
return { success: true, message: "Logged out successfully" };
} catch (error: any) {
console.error("Error logging out user:", error.response?.data || error.message);
// Create a proper error object that preserves the original response
const enhancedError: ApiError = new Error(
error.response?.data?.message || error.response?.data?.error || "Failed to log out. Please try again."
);
enhancedError.response = error.response;
throw enhancedError;
}
}

View File

@ -0,0 +1,6 @@
import EmailVerification from "@/app/auth/emailVerification";
export default function VerifyEmailPage() {
return <EmailVerification />;
}

View File

@ -7,14 +7,12 @@ import { useRouter } from "next/navigation"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label" import { Label } from "@/components/ui/label"
import { Eye, EyeOff, Loader2 } from "lucide-react" import { Eye, EyeOff, Loader2, Shield } from "lucide-react"
import { useAuth } from "@/contexts/auth-context" import { useAuth } from "@/contexts/auth-context"
import { loginUser } from "@/components/apis/authenticationHandler"
import { setTokens } from "@/components/apis/authApiClients"
interface SignInFormProps { export function SignInForm() {
onToggleMode: () => void
}
export function SignInForm({ onToggleMode }: SignInFormProps) {
const [showPassword, setShowPassword] = useState(false) const [showPassword, setShowPassword] = useState(false)
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState("") const [error, setError] = useState("")
@ -23,23 +21,46 @@ export function SignInForm({ onToggleMode }: SignInFormProps) {
password: "", password: "",
}) })
const { login } = useAuth()
const router = useRouter() const router = useRouter()
const { setUserFromApi } = useAuth()
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault() e.preventDefault()
setError("") setError("")
setIsLoading(true)
if (!formData.email || !formData.password) {
setError("Please fill in all required fields.")
return
}
setIsLoading(true)
try { try {
const success = await login(formData.email, formData.password) const response = await loginUser(formData.email, formData.password)
if (success) {
if (response && response.data && response.data.tokens && response.data.user) {
setTokens(response.data.tokens.accessToken, response.data.tokens.refreshToken)
// Set user in context so header updates immediately
setUserFromApi(response.data.user)
// Persist for refresh
localStorage.setItem("codenuk_user", JSON.stringify(response.data.user))
// Go to main app
router.push("/") router.push("/")
} else { } else {
setError("Invalid email or password") setError("Invalid response from server. Please try again.")
}
} catch (err: any) {
console.error('Login error:', err)
// Handle different types of errors
if (err.response?.data?.message) {
setError(err.response.data.message)
} else if (err.response?.data?.error) {
setError(err.response.data.error)
} else if (err.message) {
setError(err.message)
} else {
setError("An error occurred during login. Please try again.")
} }
} catch (err) {
setError("An error occurred. Please try again.")
} finally { } finally {
setIsLoading(false) setIsLoading(false)
} }
@ -47,7 +68,7 @@ export function SignInForm({ onToggleMode }: SignInFormProps) {
return ( return (
<div className="w-full"> <div className="w-full">
<form onSubmit={handleSubmit} className="space-y-5"> <form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="email" className="text-white text-sm">Email</Label> <Label htmlFor="email" className="text-white text-sm">Email</Label>
<Input <Input
@ -57,7 +78,7 @@ export function SignInForm({ onToggleMode }: SignInFormProps) {
value={formData.email} value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })} onChange={(e) => setFormData({ ...formData, email: e.target.value })}
required required
className="h-11 bg-white/5 border-white/10 text-white placeholder:text-white/40 focus:border-white/30 focus:ring-white/30" className="h-11 bg-white/5 border-white/10 text-white placeholder:text-white/40 focus:border-orange-400/50 focus:ring-orange-400/20 transition-all duration-200"
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
@ -70,13 +91,13 @@ export function SignInForm({ onToggleMode }: SignInFormProps) {
value={formData.password} value={formData.password}
onChange={(e) => setFormData({ ...formData, password: e.target.value })} onChange={(e) => setFormData({ ...formData, password: e.target.value })}
required required
className="h-11 bg-white/5 border-white/10 text-white placeholder:text-white/40 focus:border-white/30 focus:ring-white/30 pr-12" className="h-11 bg-white/5 border-white/10 text-white placeholder:text-white/40 focus:border-orange-400/50 focus:ring-orange-400/20 transition-all duration-200 pr-12"
/> />
<Button <Button
type="button" type="button"
variant="ghost" variant="ghost"
size="sm" size="sm"
className="absolute right-0 top-0 h-full px-3 py-2 text-white/60 hover:text-white hover:bg-white/5" className="absolute right-0 top-0 h-full px-3 py-2 text-white/60 hover:text-white hover:bg-white/5 transition-colors"
onClick={() => setShowPassword(!showPassword)} onClick={() => setShowPassword(!showPassword)}
> >
{showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />} {showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
@ -86,11 +107,18 @@ export function SignInForm({ onToggleMode }: SignInFormProps) {
{error && ( {error && (
<div className="text-red-300 text-sm text-center bg-red-900/20 p-3 rounded-md border border-red-500/30"> <div className="text-red-300 text-sm text-center bg-red-900/20 p-3 rounded-md border border-red-500/30">
{error} <div className="flex items-center justify-center space-x-2">
<Shield className="h-4 w-4" />
<span>{error}</span>
</div>
</div> </div>
)} )}
<Button type="submit" className="w-full h-11 cursor-pointer bg-white text-black hover:bg-white/90" disabled={isLoading}> <Button
type="submit"
className="w-full h-11 cursor-pointer bg-gradient-to-r from-orange-500 to-orange-600 text-white hover:from-orange-600 hover:to-orange-700 transition-all duration-200 font-semibold shadow-lg hover:shadow-orange-500/25"
disabled={isLoading}
>
{isLoading ? ( {isLoading ? (
<> <>
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> <Loader2 className="mr-2 h-4 w-4 animate-spin" />
@ -100,11 +128,6 @@ export function SignInForm({ onToggleMode }: SignInFormProps) {
"Sign In" "Sign In"
)} )}
</Button> </Button>
<div className="text-center">
<Button type="button" variant="link" onClick={onToggleMode} className="text-sm cursor-pointer text-white/70 hover:text-white">
Don&apos;t have an account? Sign up
</Button>
</div>
</form> </form>
</div> </div>
) )

View File

@ -0,0 +1,124 @@
"use client"
import { useState, useEffect } from "react"
import { useRouter, useSearchParams } from "next/navigation"
import { SignInForm } from "./signin-form"
import { Button } from "@/components/ui/button"
import { CheckCircle, AlertCircle } from "lucide-react"
export function SignInPage() {
const router = useRouter()
const searchParams = useSearchParams()
const [verificationMessage, setVerificationMessage] = useState<string | null>(null)
const [verificationType, setVerificationType] = useState<'success' | 'error'>('success')
useEffect(() => {
// Check for verification messages in URL
const verified = searchParams.get('verified')
const message = searchParams.get('message')
const error = searchParams.get('error')
if (verified === 'true') {
setVerificationMessage('Email verified successfully! You can now sign in to your account.')
setVerificationType('success')
} else if (message) {
setVerificationMessage(decodeURIComponent(message))
setVerificationType('success')
} else if (error) {
setVerificationMessage(decodeURIComponent(error))
setVerificationType('error')
}
// Clear the message after 5 seconds
if (verified || message || error) {
const timer = setTimeout(() => {
setVerificationMessage(null)
}, 5000)
return () => clearTimeout(timer)
}
}, [searchParams])
return (
<div className="min-h-screen bg-black text-white flex items-center justify-center p-6">
<div className="w-full max-w-6xl grid grid-cols-1 lg:grid-cols-2 gap-10">
{/* Left: Gradient panel with steps */}
<div className="hidden lg:block">
<div className="relative h-full rounded-3xl overflow-hidden border border-white/10 bg-gradient-to-b from-orange-400 via-orange-500 to-black p-10">
{/* subtle grain */}
<div className="pointer-events-none absolute inset-0 opacity-20 [mask-image:radial-gradient(ellipse_at_center,black_70%,transparent_100%)]">
<div className="w-full h-full bg-[radial-gradient(circle_at_1px_1px,rgba(255,255,255,0.12)_1px,transparent_0)] bg-[length:18px_18px]"></div>
</div>
{/* soft circular accents */}
<div className="pointer-events-none absolute -top-10 -right-10 w-72 h-72 rounded-full blur-3xl bg-purple-300/40"></div>
<div className="pointer-events-none absolute top-1/3 -left-10 w-56 h-56 rounded-full blur-2xl bg-indigo-300/25"></div>
<div className="pointer-events-none absolute -bottom-12 left-1/3 w-64 h-64 rounded-full blur-3xl bg-purple-400/20"></div>
<div className="relative z-10 flex h-full flex-col justify-center">
<div className="mb-12">
<p className="text-5xl font-bold text-center text-white/80">Codenuk</p>
</div>
<h2 className="text-3xl text-center font-bold mb-3">Welcome Back!</h2>
<p className="text-white/80 text-center mb-10">Sign in to access your workspace and continue building.</p>
<div className="space-y-4">
{/* Step 1 - Completed */}
<div className="bg-white/10 text-white border border-white/20 rounded-xl p-4 flex items-center">
<div className="w-9 h-9 rounded-full bg-white/20 text-white flex items-center justify-center mr-4 text-sm font-semibold">1</div>
<span className="font-medium">Sign up your account</span>
</div>
{/* Step 2 - Active */}
<div className="bg-white text-black rounded-xl p-4 flex items-center">
<div className="w-9 h-9 rounded-full bg-black text-white flex items-center justify-center mr-4 text-sm font-semibold">2</div>
<span className="font-medium">Sign in to workspace</span>
</div>
</div>
</div>
</div>
</div>
{/* Right: Form area */}
<div className="flex items-center">
<div className="w-full max-w-md ml-auto">
<div className="text-center mb-8">
<h1 className="text-2xl font-semibold text-white mb-1">Sign In Account</h1>
<p className="text-white/60">Enter your credentials to access your account.</p>
</div>
{/* Verification Message */}
{verificationMessage && (
<div className={`mb-6 p-4 rounded-lg border ${
verificationType === 'success'
? 'bg-green-500/10 border-green-500/30 text-green-300'
: 'bg-red-500/10 border-red-500/30 text-red-300'
}`}>
<div className="flex items-center space-x-2">
{verificationType === 'success' ? (
<CheckCircle className="h-4 w-4" />
) : (
<AlertCircle className="h-4 w-4" />
)}
<span className="text-sm">{verificationMessage}</span>
</div>
</div>
)}
<SignInForm />
<div className="text-center pt-4">
<div className="text-white/60 text-xs mb-1">Don't have an account?</div>
<Button
type="button"
variant="link"
onClick={() => router.push("/signup")}
className="text-orange-400 hover:text-orange-300 font-medium transition-colors text-sm p-0 h-auto cursor-pointer"
>
Create a new account
</Button>
</div>
</div>
</div>
</div>
</div>
)
}

View File

@ -8,23 +8,27 @@ import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label" import { Label } from "@/components/ui/label"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Eye, EyeOff, Loader2 } from "lucide-react" import { Eye, EyeOff, Loader2, User, Mail, Lock, Shield } from "lucide-react"
import { useAuth } from "@/contexts/auth-context" import { useAuth } from "@/contexts/auth-context"
import { registerUser } from "../apis/authenticationHandler"
interface SignUpFormProps { interface SignUpFormProps {
onToggleMode: () => void onSignUpSuccess?: () => void
} }
export function SignUpForm({ onToggleMode }: SignUpFormProps) { export function SignUpForm({ onSignUpSuccess }: SignUpFormProps) {
const [showPassword, setShowPassword] = useState(false) const [showPassword, setShowPassword] = useState(false)
const [showConfirmPassword, setShowConfirmPassword] = useState(false) const [showConfirmPassword, setShowConfirmPassword] = useState(false)
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState("") const [error, setError] = useState("")
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: "", username: "",
first_name: "",
last_name: "",
email: "", email: "",
password: "", password: "",
confirmPassword: "", confirmPassword: "",
role: "user" // default role, adjust as needed
}) })
const { signup } = useAuth() const { signup } = useAuth()
@ -34,115 +38,216 @@ export function SignUpForm({ onToggleMode }: SignUpFormProps) {
e.preventDefault() e.preventDefault()
setError("") setError("")
// Validation
if (formData.password !== formData.confirmPassword) { if (formData.password !== formData.confirmPassword) {
setError("Passwords don't match") setError("Passwords don't match")
return return
} }
if (!formData.username || !formData.first_name || !formData.last_name || !formData.email || !formData.password) {
setError("Please fill in all required fields.")
return
}
// Password strength validation
if (formData.password.length < 8) {
setError("Password must be at least 8 characters long")
return
}
setIsLoading(true) setIsLoading(true)
try { try {
const success = await signup(formData.name, formData.email, formData.password) const response = await registerUser({
if (success) { username: formData.username,
router.push("/") email: formData.email,
password: formData.password,
first_name: formData.first_name,
last_name: formData.last_name,
role: formData.role
})
if (response.success) {
// Call success callback if provided
if (onSignUpSuccess) {
onSignUpSuccess()
} else { } else {
setError("Failed to create account. Please try again.") // Default behavior - redirect to signin with message
router.push("/signin?message=Account created successfully! Please check your email to verify your account.")
}
} else {
setError(response.message || "Failed to create account. Please try again.")
}
} catch (err: any) {
console.error('Signup error:', err)
// Handle different types of errors
if (err.response?.data?.message) {
setError(err.response.data.message)
} else if (err.message) {
setError(err.message)
} else {
setError("An error occurred during registration. Please try again.")
} }
} catch (err) {
setError("An error occurred. Please try again.")
} finally { } finally {
setIsLoading(false) setIsLoading(false)
} }
} }
return ( return (
<Card className="w-full max-w-md bg-transparent border-0 shadow-none"> <div className="w-full max-w-md">
<CardHeader className="space-y-1 p-0 mb-2"> <form onSubmit={handleSubmit} className="space-y-4">
<CardTitle className="sr-only">Sign Up</CardTitle> {/* Personal Information Section */}
<CardDescription className="sr-only">Create your account to get started</CardDescription> <div className="space-y-3">
</CardHeader> <div className="flex items-center space-x-2 mb-2">
<CardContent className="p-0"> <User className="h-4 w-4 text-orange-400" />
<form onSubmit={handleSubmit} className="space-y-5"> <h3 className="text-base font-semibold text-white">Personal Information</h3>
<div className="space-y-2"> </div>
<Label htmlFor="name" className="text-white text-sm">Full Name</Label>
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1">
<Label htmlFor="first_name" className="text-white/80 text-xs font-medium">First Name</Label>
<Input <Input
id="name" id="first_name"
type="text" type="text"
placeholder="Enter your full name" placeholder="John"
value={formData.name} value={formData.first_name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })} onChange={(e) => setFormData({ ...formData, first_name: e.target.value })}
required required
className="h-11 bg-white/5 border-white/10 text-white placeholder:text-white/40 focus:border-white/30 focus:ring-white/30" className="h-10 bg-white/5 border-white/10 text-white placeholder:text-white/30 focus:border-orange-400/50 focus:ring-orange-400/20 transition-all duration-200 text-sm"
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-1">
<Label htmlFor="email" className="text-white text-sm">Email</Label> <Label htmlFor="last_name" className="text-white/80 text-xs font-medium">Last Name</Label>
<Input
id="last_name"
type="text"
placeholder="Doe"
value={formData.last_name}
onChange={(e) => setFormData({ ...formData, last_name: e.target.value })}
required
className="h-10 bg-white/5 border-white/10 text-white placeholder:text-white/30 focus:border-orange-400/50 focus:ring-orange-400/20 transition-all duration-200 text-sm"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1">
<Label htmlFor="username" className="text-white/80 text-xs font-medium">Username</Label>
<Input
id="username"
type="text"
placeholder="johndoe123"
value={formData.username}
onChange={(e) => setFormData({ ...formData, username: e.target.value })}
required
className="h-10 bg-white/5 border-white/10 text-white placeholder:text-white/30 focus:border-orange-400/50 focus:ring-orange-400/20 transition-all duration-200 text-sm"
/>
</div>
<div className="space-y-1">
<Label htmlFor="role" className="text-white/80 text-xs font-medium">Role</Label>
<Input
id="role"
type="text"
placeholder="user"
value={formData.role}
onChange={(e) => setFormData({ ...formData, role: e.target.value })}
required
className="h-10 bg-white/5 border-white/10 text-white placeholder:text-white/30 focus:border-orange-400/50 focus:ring-orange-400/20 transition-all duration-200 text-sm"
/>
</div>
</div>
</div>
{/* Contact Information Section */}
<div className="space-y-3">
<div className="flex items-center space-x-2 mb-2">
<Mail className="h-4 w-4 text-orange-400" />
<h3 className="text-base font-semibold text-white">Contact Information</h3>
</div>
<div className="space-y-1">
<Label htmlFor="email" className="text-white/80 text-xs font-medium">Email Address</Label>
<Input <Input
id="email" id="email"
type="email" type="email"
placeholder="Enter your email" placeholder="john.doe@example.com"
value={formData.email} value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })} onChange={(e) => setFormData({ ...formData, email: e.target.value })}
required required
className="h-11 bg-white/5 border-white/10 text-white placeholder:text-white/40 focus:border-white/30 focus:ring-white/30" className="h-10 bg-white/5 border-white/10 text-white placeholder:text-white/30 focus:border-orange-400/50 focus:ring-orange-400/20 transition-all duration-200 text-sm"
/> />
</div> </div>
<div className="space-y-2"> </div>
<Label htmlFor="password" className="text-white text-sm">Password</Label>
{/* Security Section */}
<div className="space-y-3">
<div className="flex items-center space-x-2 mb-2">
<Lock className="h-4 w-4 text-orange-400" />
<h3 className="text-base font-semibold text-white">Security</h3>
</div>
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1">
<Label htmlFor="password" className="text-white/80 text-xs font-medium">Password</Label>
<div className="relative"> <div className="relative">
<Input <Input
id="password" id="password"
type={showPassword ? "text" : "password"} type={showPassword ? "text" : "password"}
placeholder="Enter your password" placeholder="Password"
value={formData.password} value={formData.password}
onChange={(e) => setFormData({ ...formData, password: e.target.value })} onChange={(e) => setFormData({ ...formData, password: e.target.value })}
required required
className="h-11 bg-white/5 border-white/10 text-white placeholder:text-white/40 focus:border-white/30 focus:ring-white/30 pr-12" className="h-10 bg-white/5 border-white/10 text-white placeholder:text-white/30 focus:border-orange-400/50 focus:ring-orange-400/20 transition-all duration-200 text-sm pr-10"
/> />
<Button <Button
type="button" type="button"
variant="ghost" variant="ghost"
size="sm" size="sm"
className="absolute right-0 top-0 h-full px-3 py-2 text-white/60 hover:text-white hover:bg-white/5" className="absolute right-0 top-0 h-full px-2 py-1 text-white/60 hover:text-white hover:bg-white/5 transition-colors"
onClick={() => setShowPassword(!showPassword)} onClick={() => setShowPassword(!showPassword)}
> >
{showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />} {showPassword ? <EyeOff className="h-3 w-3" /> : <Eye className="h-3 w-3" />}
</Button>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="confirmPassword" className="text-white text-sm">Confirm Password</Label>
<div className="relative">
<Input
id="confirmPassword"
type={showConfirmPassword ? "text" : "password"}
placeholder="Confirm your password"
value={formData.confirmPassword}
onChange={(e) => setFormData({ ...formData, confirmPassword: e.target.value })}
required
className="h-11 bg-white/5 border-white/10 text-white placeholder:text-white/40 focus:border-white/30 focus:ring-white/30 pr-12"
/>
<Button
type="button"
variant="ghost"
size="sm"
className="absolute right-0 top-0 h-full px-3 py-2 text-white/60 hover:text-white hover:bg-white/5"
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
>
{showConfirmPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</Button> </Button>
</div> </div>
</div> </div>
<div className="space-y-1">
<Label htmlFor="confirmPassword" className="text-white/80 text-xs font-medium">Confirm</Label>
<div className="relative">
<Input
id="confirmPassword"
type={showConfirmPassword ? "text" : "password"}
placeholder="Confirm"
value={formData.confirmPassword}
onChange={(e) => setFormData({ ...formData, confirmPassword: e.target.value })}
required
className="h-10 bg-white/5 border-white/10 text-white placeholder:text-white/30 focus:border-orange-400/50 focus:ring-orange-400/20 transition-all duration-200 text-sm pr-10"
/>
<Button
type="button"
variant="ghost"
size="sm"
className="absolute right-0 top-0 h-full px-2 py-1 text-white/60 hover:text-white hover:bg-white/5 transition-colors"
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
>
{showConfirmPassword ? <EyeOff className="h-3 w-3" /> : <Eye className="h-3 w-3" />}
</Button>
</div>
</div>
</div>
</div>
{error && ( {error && (
<div className="text-red-300 text-sm text-center bg-red-900/20 p-3 rounded-md border border-red-500/30"> <div className="text-red-300 text-xs text-center bg-red-900/20 p-3 rounded-md border border-red-500/30">
{error} <div className="flex items-center justify-center space-x-1">
<Shield className="h-3 w-3" />
<span>{error}</span>
</div>
</div> </div>
)} )}
<Button <Button
type="submit" type="submit"
className="w-full h-11 cursor-pointer bg-white text-black hover:bg-white/90" className="w-full h-10 cursor-pointer bg-gradient-to-r from-orange-500 to-orange-600 text-white hover:from-orange-600 hover:to-orange-700 transition-all duration-200 font-semibold text-sm shadow-lg hover:shadow-orange-500/25"
disabled={isLoading} disabled={isLoading}
> >
{isLoading ? ( {isLoading ? (
@ -151,22 +256,10 @@ export function SignUpForm({ onToggleMode }: SignUpFormProps) {
Creating Account... Creating Account...
</> </>
) : ( ) : (
"Sign Up" "Create Account"
)} )}
</Button> </Button>
<div className="text-center">
<Button
type="button"
variant="link"
onClick={onToggleMode}
className="text-sm cursor-pointer text-white/70 hover:text-white"
>
Already have an account? Sign in
</Button>
</div>
</form> </form>
</CardContent> </div>
</Card>
) )
} }

View File

@ -0,0 +1,157 @@
"use client"
import { useState } from "react"
import { useRouter } from "next/navigation"
import { SignUpForm } from "./signup-form"
import { Button } from "@/components/ui/button"
import { CheckCircle, ArrowRight } from "lucide-react"
export function SignUpPage() {
const [isSuccess, setIsSuccess] = useState(false)
const router = useRouter()
const handleSignUpSuccess = () => {
setIsSuccess(true)
// Redirect to signin after 3 seconds
setTimeout(() => {
router.push("/signin?message=Please check your email to verify your account")
}, 3000)
}
if (isSuccess) {
return (
<div className="min-h-screen bg-black text-white flex items-center justify-center p-6">
<div className="w-full max-w-6xl grid grid-cols-1 lg:grid-cols-2 gap-10">
{/* Left: Gradient panel */}
<div className="hidden lg:block">
<div className="relative h-full rounded-3xl overflow-hidden border border-white/10 bg-gradient-to-b from-orange-400 via-orange-500 to-black p-10">
{/* subtle grain */}
<div className="pointer-events-none absolute inset-0 opacity-20 [mask-image:radial-gradient(ellipse_at_center,black_70%,transparent_100%)]">
<div className="w-full h-full bg-[radial-gradient(circle_at_1px_1px,rgba(255,255,255,0.12)_1px,transparent_0)] bg-[length:18px_18px]"></div>
</div>
{/* soft circular accents */}
<div className="pointer-events-none absolute -top-10 -right-10 w-72 h-72 rounded-full blur-3xl bg-purple-300/40"></div>
<div className="pointer-events-none absolute top-1/3 -left-10 w-56 h-56 rounded-full blur-2xl bg-indigo-300/25"></div>
<div className="pointer-events-none absolute -bottom-12 left-1/3 w-64 h-64 rounded-full blur-3xl bg-purple-400/20"></div>
<div className="relative z-10 flex h-full flex-col justify-center">
<div className="mb-12">
<p className="text-5xl font-bold text-center text-white/80">Codenuk</p>
</div>
<h2 className="text-3xl text-center font-bold mb-3">Account Created!</h2>
<p className="text-white/80 text-center mb-10">Your account has been successfully created. Please verify your email to continue.</p>
<div className="space-y-4">
{/* Step 1 - Completed */}
<div className="bg-white text-black rounded-xl p-4 flex items-center">
<div className="w-9 h-9 rounded-full bg-green-500 text-white flex items-center justify-center mr-4 text-sm font-semibold">
<CheckCircle className="h-5 w-5" />
</div>
<span className="font-medium">Sign up completed</span>
</div>
{/* Step 2 - Next */}
<div className="bg-white/10 text-white border border-white/20 rounded-xl p-4 flex items-center">
<div className="w-9 h-9 rounded-full bg-white/20 text-white flex items-center justify-center mr-4 text-sm font-semibold">2</div>
<span className="font-medium">Verify email & sign in</span>
</div>
</div>
</div>
</div>
</div>
{/* Right: Success message */}
<div className="flex items-center">
<div className="w-full max-w-md ml-auto text-center">
<div className="mb-8">
<div className="w-20 h-20 bg-green-500 rounded-full flex items-center justify-center mx-auto mb-6">
<CheckCircle className="h-10 w-10 text-white" />
</div>
<h1 className="text-2xl font-semibold text-white mb-2">Account Created Successfully!</h1>
<p className="text-white/60 mb-6">We've sent a verification email to your inbox. Please check your email and click the verification link to activate your account.</p>
<div className="bg-orange-500/10 border border-orange-500/30 rounded-lg p-4 mb-6">
<p className="text-orange-300 text-sm">
<strong>Next step:</strong> Check your email and click the verification link, then sign in to your account.
</p>
</div>
<Button
onClick={() => router.push("/signin")}
className="bg-gradient-to-r from-orange-500 to-orange-600 text-white hover:from-orange-600 hover:to-orange-700"
>
Go to Sign In
<ArrowRight className="ml-2 h-4 w-4" />
</Button>
</div>
</div>
</div>
</div>
</div>
)
}
return (
<div className="min-h-screen bg-black text-white flex items-center justify-center p-6">
<div className="w-full max-w-6xl grid grid-cols-1 lg:grid-cols-2 gap-10">
{/* Left: Gradient panel with steps */}
<div className="hidden lg:block">
<div className="relative h-full rounded-3xl overflow-hidden border border-white/10 bg-gradient-to-b from-orange-400 via-orange-500 to-black p-10">
{/* subtle grain */}
<div className="pointer-events-none absolute inset-0 opacity-20 [mask-image:radial-gradient(ellipse_at_center,black_70%,transparent_100%)]">
<div className="w-full h-full bg-[radial-gradient(circle_at_1px_1px,rgba(255,255,255,0.12)_1px,transparent_0)] bg-[length:18px_18px]"></div>
</div>
{/* soft circular accents */}
<div className="pointer-events-none absolute -top-10 -right-10 w-72 h-72 rounded-full blur-3xl bg-purple-300/40"></div>
<div className="pointer-events-none absolute top-1/3 -left-10 w-56 h-56 rounded-full blur-2xl bg-indigo-300/25"></div>
<div className="pointer-events-none absolute -bottom-12 left-1/3 w-64 h-64 rounded-full blur-3xl bg-purple-400/20"></div>
<div className="relative z-10 flex h-full flex-col justify-center">
<div className="mb-12">
<p className="text-5xl font-bold text-center text-white/80">Codenuk</p>
</div>
<h2 className="text-3xl text-center font-bold mb-3">Get Started with Us</h2>
<p className="text-white/80 text-center mb-10">Complete these easy steps to register your account.</p>
<div className="space-y-4">
{/* Step 1 - Active */}
<div className="bg-white text-black rounded-xl p-4 flex items-center">
<div className="w-9 h-9 rounded-full bg-black text-white flex items-center justify-center mr-4 text-sm font-semibold">1</div>
<span className="font-medium">Sign up your account</span>
</div>
{/* Step 2 - Next */}
<div className="bg-white/10 text-white border border-white/20 rounded-xl p-4 flex items-center">
<div className="w-9 h-9 rounded-full bg-white/20 text-white flex items-center justify-center mr-4 text-sm font-semibold">2</div>
<span className="font-medium">Verify email & sign in</span>
</div>
</div>
</div>
</div>
</div>
{/* Right: Form area */}
<div className="flex items-center">
<div className="w-full max-w-md ml-auto">
<div className="text-center mb-8">
<h1 className="text-2xl font-semibold text-white mb-1">Sign Up Account</h1>
<p className="text-white/60">Enter your personal data to create your account.</p>
</div>
<SignUpForm onSignUpSuccess={handleSignUpSuccess} />
<div className="text-center pt-4">
<div className="text-white/60 text-xs mb-1">Already have an account?</div>
<Button
type="button"
variant="link"
onClick={() => router.push("/signin")}
className="text-orange-400 hover:text-orange-300 font-medium transition-colors text-sm p-0 h-auto cursor-pointer"
>
Sign in to your account
</Button>
</div>
</div>
</div>
</div>
</div>
)
}

View File

@ -0,0 +1,210 @@
"use client"
import { useState } from "react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Textarea } from "@/components/ui/textarea"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { DatabaseTemplate } from "@/lib/template-service"
import { Plus, X, Save } from "lucide-react"
interface CustomTemplateFormProps {
onSubmit: (templateData: Partial<DatabaseTemplate>) => Promise<void>
onCancel: () => void
}
export function CustomTemplateForm({ onSubmit, onCancel }: CustomTemplateFormProps) {
const [formData, setFormData] = useState({
type: "",
title: "",
description: "",
category: "",
icon: "",
gradient: "",
border: "",
text: "",
subtext: ""
})
const [loading, setLoading] = useState(false)
const categories = [
"Food Delivery",
"E-commerce",
"SaaS Platform",
"Mobile App",
"Dashboard",
"CRM System",
"Learning Platform",
"Healthcare",
"Real Estate",
"Travel",
"Entertainment",
"Finance",
"Social Media",
"Marketplace",
"Other"
]
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setLoading(true)
try {
await onSubmit(formData)
} catch (error) {
console.error('Error creating template:', error)
} finally {
setLoading(false)
}
}
const handleInputChange = (field: string, value: string) => {
setFormData(prev => ({
...prev,
[field]: value
}))
}
return (
<Card className="bg-white/5 border-white/10">
<CardHeader>
<CardTitle className="text-white flex items-center">
<Plus className="mr-2 h-5 w-5" />
Create Custom Template
</CardTitle>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-sm font-medium text-white">Template Type *</label>
<Input
placeholder="e.g., multi_restaurant_food_delivery"
value={formData.type}
onChange={(e) => handleInputChange('type', e.target.value)}
className="bg-white/5 border-white/10 text-white placeholder:text-white/40"
required
/>
<p className="text-xs text-white/60">Unique identifier for the template</p>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-white">Title *</label>
<Input
placeholder="e.g., Multi-Restaurant Food Delivery App"
value={formData.title}
onChange={(e) => handleInputChange('title', e.target.value)}
className="bg-white/5 border-white/10 text-white placeholder:text-white/40"
required
/>
</div>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-white">Description *</label>
<Textarea
placeholder="Describe your template and its key features..."
value={formData.description}
onChange={(e) => handleInputChange('description', e.target.value)}
className="bg-white/5 border-white/10 text-white placeholder:text-white/40 min-h-[100px]"
required
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-sm font-medium text-white">Category *</label>
<Select value={formData.category} onValueChange={(value) => handleInputChange('category', value)}>
<SelectTrigger className="bg-white/5 border-white/10 text-white">
<SelectValue placeholder="Select a category" />
</SelectTrigger>
<SelectContent className="bg-gray-900 border-white/10">
{categories.map((category) => (
<SelectItem key={category} value={category} className="text-white">
{category}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-white">Icon (optional)</label>
<Input
placeholder="e.g., restaurant, shopping-cart, users"
value={formData.icon}
onChange={(e) => handleInputChange('icon', e.target.value)}
className="bg-white/5 border-white/10 text-white placeholder:text-white/40"
/>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="space-y-2">
<label className="text-sm font-medium text-white">Gradient (optional)</label>
<Input
placeholder="e.g., from-orange-400 to-red-500"
value={formData.gradient}
onChange={(e) => handleInputChange('gradient', e.target.value)}
className="bg-white/5 border-white/10 text-white placeholder:text-white/40"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-white">Border (optional)</label>
<Input
placeholder="e.g., border-orange-500"
value={formData.border}
onChange={(e) => handleInputChange('border', e.target.value)}
className="bg-white/5 border-white/10 text-white placeholder:text-white/40"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-white">Text Color (optional)</label>
<Input
placeholder="e.g., text-orange-500"
value={formData.text}
onChange={(e) => handleInputChange('text', e.target.value)}
className="bg-white/5 border-white/10 text-white placeholder:text-white/40"
/>
</div>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-white">Subtext (optional)</label>
<Input
placeholder="e.g., Perfect for food delivery startups"
value={formData.subtext}
onChange={(e) => handleInputChange('subtext', e.target.value)}
className="bg-white/5 border-white/10 text-white placeholder:text-white/40"
/>
</div>
<div className="flex justify-end space-x-3 pt-4">
<Button
type="button"
variant="outline"
onClick={onCancel}
className="border-white/20 text-white hover:bg-white/10 cursor-pointer"
disabled={loading}
>
<X className="mr-2 h-4 w-4" />
Cancel
</Button>
<Button
type="submit"
className="bg-orange-500 hover:bg-orange-400 text-black font-semibold cursor-pointer"
disabled={loading}
>
<Save className="mr-2 h-4 w-4" />
{loading ? "Creating..." : "Create Template"}
</Button>
</div>
</form>
</CardContent>
</Card>
)
}

View File

@ -0,0 +1,60 @@
"use client"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { AlertTriangle, Trash2, X } from "lucide-react"
interface DeleteConfirmationDialogProps {
templateTitle: string
onConfirm: () => Promise<void>
onCancel: () => void
loading?: boolean
}
export function DeleteConfirmationDialog({
templateTitle,
onConfirm,
onCancel,
loading = false
}: DeleteConfirmationDialogProps) {
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<Card className="bg-white/5 border-white/10 max-w-md w-full mx-4">
<CardHeader>
<CardTitle className="text-white flex items-center">
<AlertTriangle className="mr-2 h-5 w-5 text-red-400" />
Delete Template
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<p className="text-white/80">
Are you sure you want to delete the template <strong className="text-white">"{templateTitle}"</strong>?
</p>
<p className="text-white/60 text-sm">
This action cannot be undone. The template will be permanently removed from the database.
</p>
<div className="flex justify-end space-x-3 pt-4">
<Button
variant="outline"
onClick={onCancel}
className="border-white/20 text-white hover:bg-white/10"
disabled={loading}
>
<X className="mr-2 h-4 w-4" />
Cancel
</Button>
<Button
onClick={onConfirm}
className="bg-red-500 hover:bg-red-400 text-white"
disabled={loading}
>
<Trash2 className="mr-2 h-4 w-4" />
{loading ? "Deleting..." : "Delete Template"}
</Button>
</div>
</CardContent>
</Card>
</div>
)
}

View File

@ -0,0 +1,225 @@
"use client"
import { useState, useEffect } from "react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Textarea } from "@/components/ui/textarea"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { DatabaseTemplate } from "@/lib/template-service"
import { Edit, X, Save } from "lucide-react"
interface EditTemplateFormProps {
template: DatabaseTemplate
onSubmit: (id: string, templateData: Partial<DatabaseTemplate>) => Promise<void>
onCancel: () => void
}
export function EditTemplateForm({ template, onSubmit, onCancel }: EditTemplateFormProps) {
const [formData, setFormData] = useState({
type: "",
title: "",
description: "",
category: "",
icon: "",
gradient: "",
border: "",
text: "",
subtext: ""
})
const [loading, setLoading] = useState(false)
const categories = [
"Food Delivery",
"E-commerce",
"SaaS Platform",
"Mobile App",
"Dashboard",
"CRM System",
"Learning Platform",
"Healthcare",
"Real Estate",
"Travel",
"Entertainment",
"Finance",
"Social Media",
"Marketplace",
"Other"
]
// Initialize form data with template values
useEffect(() => {
setFormData({
type: template.type || "",
title: template.title || "",
description: template.description || "",
category: template.category || "",
icon: template.icon || "",
gradient: template.gradient || "",
border: template.border || "",
text: template.text || "",
subtext: template.subtext || ""
})
}, [template])
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setLoading(true)
try {
await onSubmit(template.id, formData)
} catch (error) {
console.error('Error updating template:', error)
} finally {
setLoading(false)
}
}
const handleInputChange = (field: string, value: string) => {
setFormData(prev => ({
...prev,
[field]: value
}))
}
return (
<Card className="bg-white/5 border-white/10">
<CardHeader>
<CardTitle className="text-white flex items-center">
<Edit className="mr-2 h-5 w-5" />
Edit Template: {template.title}
</CardTitle>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-sm font-medium text-white">Template Type *</label>
<Input
placeholder="e.g., multi_restaurant_food_delivery"
value={formData.type}
onChange={(e) => handleInputChange('type', e.target.value)}
className="bg-white/5 border-white/10 text-white placeholder:text-white/40"
required
/>
<p className="text-xs text-white/60">Unique identifier for the template</p>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-white">Title *</label>
<Input
placeholder="e.g., Multi-Restaurant Food Delivery App"
value={formData.title}
onChange={(e) => handleInputChange('title', e.target.value)}
className="bg-white/5 border-white/10 text-white placeholder:text-white/40"
required
/>
</div>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-white">Description *</label>
<Textarea
placeholder="Describe your template and its key features..."
value={formData.description}
onChange={(e) => handleInputChange('description', e.target.value)}
className="bg-white/5 border-white/10 text-white placeholder:text-white/40 min-h-[100px]"
required
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-sm font-medium text-white">Category *</label>
<Select value={formData.category} onValueChange={(value) => handleInputChange('category', value)}>
<SelectTrigger className="bg-white/5 border-white/10 text-white">
<SelectValue placeholder="Select a category" />
</SelectTrigger>
<SelectContent className="bg-gray-900 border-white/10">
{categories.map((category) => (
<SelectItem key={category} value={category} className="text-white">
{category}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-white">Icon (optional)</label>
<Input
placeholder="e.g., restaurant, shopping-cart, users"
value={formData.icon}
onChange={(e) => handleInputChange('icon', e.target.value)}
className="bg-white/5 border-white/10 text-white placeholder:text-white/40"
/>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="space-y-2">
<label className="text-sm font-medium text-white">Gradient (optional)</label>
<Input
placeholder="e.g., from-orange-400 to-red-500"
value={formData.gradient}
onChange={(e) => handleInputChange('gradient', e.target.value)}
className="bg-white/5 border-white/10 text-white placeholder:text-white/40"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-white">Border (optional)</label>
<Input
placeholder="e.g., border-orange-500"
value={formData.border}
onChange={(e) => handleInputChange('border', e.target.value)}
className="bg-white/5 border-white/10 text-white placeholder:text-white/40"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-white">Text Color (optional)</label>
<Input
placeholder="e.g., text-orange-500"
value={formData.text}
onChange={(e) => handleInputChange('text', e.target.value)}
className="bg-white/5 border-white/10 text-white placeholder:text-white/40"
/>
</div>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-white">Subtext (optional)</label>
<Input
placeholder="e.g., Perfect for food delivery startups"
value={formData.subtext}
onChange={(e) => handleInputChange('subtext', e.target.value)}
className="bg-white/5 border-white/10 text-white placeholder:text-white/40"
/>
</div>
<div className="flex justify-end space-x-3 pt-4">
<Button
type="button"
variant="outline"
onClick={onCancel}
className="border-white/20 text-white hover:bg-white/10"
disabled={loading}
>
<X className="mr-2 h-4 w-4" />
Cancel
</Button>
<Button
type="submit"
className="bg-orange-500 hover:bg-orange-400 text-black font-semibold"
disabled={loading}
>
<Save className="mr-2 h-4 w-4" />
{loading ? "Updating..." : "Update Template"}
</Button>
</div>
</form>
</CardContent>
</Card>
)
}

View File

@ -13,7 +13,7 @@ export function AppLayout({ children }: AppLayoutProps) {
const pathname = usePathname() const pathname = usePathname()
// Don't show header on auth pages // Don't show header on auth pages
const isAuthPage = pathname === "/auth" const isAuthPage = pathname === "/auth" || pathname === "/signin" || pathname === "/signup"
// Show loading state while checking auth // Show loading state while checking auth
if (isLoading) { if (isLoading) {
@ -39,7 +39,7 @@ export function AppLayout({ children }: AppLayoutProps) {
) )
} }
// For unauthenticated users on non-auth pages, redirect to auth // For unauthenticated users on non-auth pages, show header but redirect to auth
return ( return (
<> <>
<Header /> <Header />

View File

@ -1,12 +1,17 @@
"use client" "use client"
import { useState } from "react" import { useState, useEffect } from "react"
import Link from "next/link" import Link from "next/link"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge" import { Badge } from "@/components/ui/badge"
import { ArrowRight, Plus, Globe, BarChart3, Zap, Code, Search, Star, Clock, Users, Layers } from "lucide-react" import { ArrowRight, Plus, Globe, BarChart3, Zap, Code, Search, Star, Clock, Users, Layers, AlertCircle, Edit, Trash2 } from "lucide-react"
import { useTemplates } from "@/hooks/useTemplates"
import { CustomTemplateForm } from "@/components/custom-template-form"
import { EditTemplateForm } from "@/components/edit-template-form"
import { DeleteConfirmationDialog } from "@/components/delete-confirmation-dialog"
import { DatabaseTemplate, TemplateFeature } from "@/lib/template-service"
interface Template { interface Template {
id: string id: string
@ -19,56 +24,74 @@ interface Template {
techStack: string[] techStack: string[]
popularity?: number popularity?: number
lastUpdated?: string lastUpdated?: string
type?: string
icon?: string | null
gradient?: string | null
border?: string | null
text?: string | null
subtext?: string | null
is_active?: boolean
created_at?: string
updated_at?: string
} }
function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => void }) { function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => void }) {
const [selectedCategory, setSelectedCategory] = useState("all") const [selectedCategory, setSelectedCategory] = useState("all")
const [searchQuery, setSearchQuery] = useState("") const [searchQuery, setSearchQuery] = useState("")
const [showCustomForm, setShowCustomForm] = useState(false)
const [editingTemplate, setEditingTemplate] = useState<DatabaseTemplate | null>(null)
const [deletingTemplate, setDeletingTemplate] = useState<DatabaseTemplate | null>(null)
const [deleteLoading, setDeleteLoading] = useState(false)
const templates: Template[] = [ const { templates: dbTemplates, loading, error, getTemplatesForUI, createTemplate, updateTemplate, deleteTemplate } = useTemplates()
// Marketing Templates (10)
{ id: "marketing-website", title: "Marketing Website", description: "Professional marketing site with CMS and lead generation", category: "marketing", features: ["Content Management", "Contact Forms", "SEO Optimization", "Analytics Integration"], complexity: 2, timeEstimate: "1-2 weeks", techStack: ["Next.js", "Sanity CMS", "Tailwind CSS", "Vercel"], popularity: 95, lastUpdated: "2024-01-15" },
{ id: "landing-page", title: "Landing Page", description: "High-converting landing page with A/B testing capabilities", category: "marketing", features: ["A/B Testing", "Conversion Tracking", "Lead Capture", "Mobile Optimization"], complexity: 2, timeEstimate: "3-5 days", techStack: ["Next.js", "Tailwind CSS", "Google Analytics", "Mailchimp"], popularity: 88, lastUpdated: "2024-01-10" },
{ id: "blog-platform", title: "Blog Platform", description: "Content-rich blog with SEO optimization and social sharing", category: "marketing", features: ["Content Management", "SEO Tools", "Social Sharing", "Comment System"], complexity: 3, timeEstimate: "1-2 weeks", techStack: ["Next.js", "MDX", "Tailwind CSS", "Disqus"], popularity: 82, lastUpdated: "2024-01-12" },
{ id: "portfolio-site", title: "Portfolio Website", description: "Personal or agency portfolio with project showcase", category: "marketing", features: ["Project Gallery", "Contact Forms", "Blog", "Responsive Design"], complexity: 2, timeEstimate: "1-2 weeks", techStack: ["Next.js", "MDX", "Tailwind CSS", "Framer Motion"], popularity: 79, lastUpdated: "2024-01-08" },
{ id: "agency-website", title: "Agency Website", description: "Full-service agency site with team profiles and case studies", category: "marketing", features: ["Team Profiles", "Case Studies", "Service Pages", "Client Testimonials"], complexity: 3, timeEstimate: "2-3 weeks", techStack: ["Next.js", "Strapi", "Tailwind CSS", "Framer Motion"], popularity: 76, lastUpdated: "2024-01-14" },
{ id: "event-website", title: "Event Website", description: "Event promotion site with registration and ticketing", category: "marketing", features: ["Event Registration", "Ticketing", "Speaker Profiles", "Schedule Management"], complexity: 4, timeEstimate: "2-3 weeks", techStack: ["Next.js", "Stripe", "Calendar API", "Email Integration"], popularity: 73, lastUpdated: "2024-01-11" },
{ id: "restaurant-website", title: "Restaurant Website", description: "Restaurant site with menu, reservations, and online ordering", category: "marketing", features: ["Menu Display", "Online Reservations", "Order System", "Location Info"], complexity: 3, timeEstimate: "1-2 weeks", techStack: ["Next.js", "Reservation API", "Payment Processing", "Google Maps"], popularity: 70, lastUpdated: "2024-01-09" },
{ id: "nonprofit-website", title: "Nonprofit Website", description: "Nonprofit organization site with donation and volunteer management", category: "marketing", features: ["Donation Processing", "Volunteer Registration", "Event Management", "Impact Tracking"], complexity: 3, timeEstimate: "2-3 weeks", techStack: ["Next.js", "Stripe", "Volunteer API", "Analytics"], popularity: 67, lastUpdated: "2024-01-13" },
{ id: "real-estate-website", title: "Real Estate Website", description: "Property listing site with search and contact features", category: "marketing", features: ["Property Listings", "Search Filters", "Contact Forms", "Virtual Tours"], complexity: 4, timeEstimate: "2-4 weeks", techStack: ["Next.js", "Property API", "Map Integration", "Image Gallery"], popularity: 64, lastUpdated: "2024-01-07" },
{ id: "personal-brand", title: "Personal Brand Site", description: "Personal branding site for professionals and creators", category: "marketing", features: ["About Page", "Services", "Testimonials", "Contact Integration"], complexity: 2, timeEstimate: "1 week", techStack: ["Next.js", "Tailwind CSS", "Contact Forms", "Social Links"], popularity: 61, lastUpdated: "2024-01-06" },
// Software Templates (10) // Get templates from database and convert to UI format
{ id: "saas-platform", title: "SaaS Platform", description: "Complete SaaS application with user management, billing, and analytics", category: "software", features: ["User Authentication", "Payment Processing", "Analytics Integration", "API Management"], complexity: 5, timeEstimate: "4-6 weeks", techStack: ["Next.js", "PostgreSQL", "Stripe", "NextAuth.js"], popularity: 92, lastUpdated: "2024-01-15" }, const databaseTemplates = getTemplatesForUI()
{ id: "dashboard-app", title: "Analytics Dashboard", description: "Data visualization dashboard with real-time updates", category: "software", features: ["Data Visualization", "Real-time Updates", "User Authentication", "Export Features"], complexity: 4, timeEstimate: "3-4 weeks", techStack: ["Next.js", "Chart.js", "WebSockets", "PostgreSQL"], popularity: 89, lastUpdated: "2024-01-14" },
{ id: "mobile-app", title: "Mobile App (PWA)", description: "Progressive web app with mobile-first design", category: "software", features: ["Offline Support", "Push Notifications", "Mobile Optimization", "App Install"], complexity: 4, timeEstimate: "2-3 weeks", techStack: ["Next.js", "PWA", "Service Workers", "Push API"], popularity: 86, lastUpdated: "2024-01-13" },
{ id: "project-management", title: "Project Management Tool", description: "Team collaboration and project tracking application", category: "software", features: ["Task Management", "Team Collaboration", "Time Tracking", "Reporting"], complexity: 5, timeEstimate: "4-5 weeks", techStack: ["Next.js", "PostgreSQL", "Real-time Updates", "File Upload"], popularity: 83, lastUpdated: "2024-01-12" },
{ id: "crm-system", title: "CRM System", description: "Customer relationship management with sales pipeline", category: "software", features: ["Contact Management", "Sales Pipeline", "Email Integration", "Reporting"], complexity: 5, timeEstimate: "3-5 weeks", techStack: ["Next.js", "PostgreSQL", "Email API", "Calendar Integration"], popularity: 80, lastUpdated: "2024-01-11" },
{ id: "inventory-management", title: "Inventory Management", description: "Stock tracking and warehouse management system", category: "software", features: ["Stock Tracking", "Barcode Scanning", "Supplier Management", "Reports"], complexity: 4, timeEstimate: "3-4 weeks", techStack: ["Next.js", "PostgreSQL", "Barcode API", "PDF Generation"], popularity: 77, lastUpdated: "2024-01-10" },
{ id: "learning-platform", title: "Learning Management System", description: "Online education platform with courses and assessments", category: "software", features: ["Course Management", "Video Streaming", "Assessments", "Progress Tracking"], complexity: 5, timeEstimate: "4-6 weeks", techStack: ["Next.js", "Video API", "PostgreSQL", "Payment Processing"], popularity: 74, lastUpdated: "2024-01-09" },
{ id: "booking-system", title: "Booking System", description: "Appointment and reservation management platform", category: "software", features: ["Calendar Integration", "Payment Processing", "Notifications", "Customer Management"], complexity: 4, timeEstimate: "2-4 weeks", techStack: ["Next.js", "Calendar API", "Stripe", "Email Integration"], popularity: 71, lastUpdated: "2024-01-08" },
{ id: "chat-application", title: "Chat Application", description: "Real-time messaging platform with file sharing", category: "software", features: ["Real-time Messaging", "File Sharing", "Group Chats", "User Presence"], complexity: 4, timeEstimate: "2-3 weeks", techStack: ["Next.js", "WebSockets", "File Storage", "Real-time DB"], popularity: 68, lastUpdated: "2024-01-07" },
{ id: "api-platform", title: "API Platform", description: "RESTful API with documentation and rate limiting", category: "software", features: ["API Documentation", "Rate Limiting", "Authentication", "Monitoring"], complexity: 4, timeEstimate: "2-3 weeks", techStack: ["Next.js", "Swagger", "Redis", "Monitoring Tools"], popularity: 65, lastUpdated: "2024-01-06" },
// SEO Templates (10) // Fallback static templates if database is empty or loading
{ id: "seo-optimized-blog", title: "SEO-Optimized Blog", description: "Blog platform with advanced SEO features and schema markup", category: "seo", features: ["Schema Markup", "Meta Optimization", "Sitemap Generation", "Performance Optimization"], complexity: 3, timeEstimate: "2-3 weeks", techStack: ["Next.js", "SEO Tools", "Schema.org", "Google Search Console"], popularity: 90, lastUpdated: "2024-01-15" }, const fallbackTemplates: Template[] = [
{ id: "local-business-site", title: "Local Business Website", description: "Local SEO optimized site with Google My Business integration", category: "seo", features: ["Local SEO", "Google My Business", "Review Management", "Location Pages"], complexity: 3, timeEstimate: "1-2 weeks", techStack: ["Next.js", "Google APIs", "Review APIs", "Local Schema"], popularity: 87, lastUpdated: "2024-01-14" }, { id: "marketing-website", title: "Marketing Website", description: "Professional marketing site with CMS and lead generation", category: "Marketing", features: ["Content Management", "Contact Forms", "SEO Optimization", "Analytics Integration"], complexity: 2, timeEstimate: "1-2 weeks", techStack: ["Next.js", "Sanity CMS", "Tailwind CSS", "Vercel"], popularity: 95, lastUpdated: "2024-01-15" },
{ id: "ecommerce-seo", title: "E-commerce SEO Site", description: "Product-focused e-commerce with advanced SEO optimization", category: "seo", features: ["Product Schema", "Category Optimization", "Review Rich Snippets", "Performance"], complexity: 4, timeEstimate: "3-4 weeks", techStack: ["Next.js", "Product APIs", "Review Systems", "CDN"], popularity: 84, lastUpdated: "2024-01-13" }, { id: "saas-platform", title: "SaaS Platform", description: "Complete SaaS application with user management, billing, and analytics", category: "SaaS Platform", features: ["User Authentication", "Payment Processing", "Analytics Integration", "API Management"], complexity: 5, timeEstimate: "4-6 weeks", techStack: ["Next.js", "PostgreSQL", "Stripe", "NextAuth.js"], popularity: 92, lastUpdated: "2024-01-15" },
{ id: "news-website", title: "News Website", description: "News platform with article SEO and AMP support", category: "seo", features: ["Article Schema", "AMP Support", "Breaking News", "Social Sharing"], complexity: 4, timeEstimate: "2-4 weeks", techStack: ["Next.js", "AMP", "News APIs", "Social APIs"], popularity: 81, lastUpdated: "2024-01-12" },
{ id: "directory-website", title: "Business Directory", description: "Local business directory with search and listings", category: "seo", features: ["Business Listings", "Search Optimization", "Category Pages", "Review System"], complexity: 4, timeEstimate: "3-4 weeks", techStack: ["Next.js", "Search APIs", "Location Services", "Review APIs"], popularity: 78, lastUpdated: "2024-01-11" },
{ id: "recipe-website", title: "Recipe Website", description: "Recipe platform with rich snippets and cooking schema", category: "seo", features: ["Recipe Schema", "Nutrition Info", "Cooking Times", "User Ratings"], complexity: 3, timeEstimate: "2-3 weeks", techStack: ["Next.js", "Recipe APIs", "Nutrition APIs", "Rating System"], popularity: 75, lastUpdated: "2024-01-10" },
{ id: "job-board", title: "Job Board", description: "Job listing platform with structured data for search engines", category: "seo", features: ["Job Schema", "Search Filters", "Application Tracking", "Company Profiles"], complexity: 4, timeEstimate: "3-4 weeks", techStack: ["Next.js", "Job APIs", "Application System", "Company APIs"], popularity: 72, lastUpdated: "2024-01-09" },
{ id: "review-website", title: "Review Website", description: "Product/service review platform with rich snippets", category: "seo", features: ["Review Schema", "Rating System", "Comparison Tools", "User Profiles"], complexity: 4, timeEstimate: "2-4 weeks", techStack: ["Next.js", "Review APIs", "Rating System", "Comparison Tools"], popularity: 69, lastUpdated: "2024-01-08" },
{ id: "travel-website", title: "Travel Website", description: "Travel guide with location-based SEO and booking integration", category: "seo", features: ["Location Schema", "Travel Guides", "Booking Integration", "Photo Galleries"], complexity: 4, timeEstimate: "3-4 weeks", techStack: ["Next.js", "Travel APIs", "Booking APIs", "Map Integration"], popularity: 66, lastUpdated: "2024-01-07" },
{ id: "healthcare-website", title: "Healthcare Website", description: "Medical practice website with health-focused SEO", category: "seo", features: ["Medical Schema", "Appointment Booking", "Health Articles", "Doctor Profiles"], complexity: 3, timeEstimate: "2-3 weeks", techStack: ["Next.js", "Medical APIs", "Booking System", "Content Management"], popularity: 63, lastUpdated: "2024-01-06" },
] ]
const categories = [ // Use database templates if available, otherwise fallback
{ id: "all", name: "All Templates", icon: Globe, count: templates.length }, const templates: Template[] = databaseTemplates.length > 0 ? databaseTemplates : fallbackTemplates
{ id: "marketing", name: "Marketing & Branding", icon: Zap, count: templates.filter((t) => t.category === "marketing").length },
{ id: "software", name: "Software & Tools", icon: Code, count: templates.filter((t) => t.category === "software").length }, // Generate dynamic categories from templates
{ id: "seo", name: "SEO & Content", icon: BarChart3, count: templates.filter((t) => t.category === "seo").length }, const getCategories = () => {
] const categoryMap = new Map<string, { name: string; icon: any; count: number }>()
// Add "All Templates" category
categoryMap.set("all", { name: "All Templates", icon: Globe, count: templates.length })
// Add categories from templates
templates.forEach(template => {
if (!categoryMap.has(template.category)) {
// Choose icon based on category name
let icon = Code // default
if (template.category.toLowerCase().includes('marketing') || template.category.toLowerCase().includes('branding')) {
icon = Zap
} else if (template.category.toLowerCase().includes('seo') || template.category.toLowerCase().includes('content')) {
icon = BarChart3
} else if (template.category.toLowerCase().includes('food') || template.category.toLowerCase().includes('delivery')) {
icon = Users
}
categoryMap.set(template.category, {
name: template.category,
icon,
count: templates.filter(t => t.category === template.category).length
})
}
})
return Array.from(categoryMap.entries()).map(([id, data]) => ({
id,
...data
}))
}
const categories = getCategories()
const filteredTemplates = templates.filter((template) => { const filteredTemplates = templates.filter((template) => {
const matchesCategory = selectedCategory === "all" || template.category === selectedCategory const matchesCategory = selectedCategory === "all" || template.category === selectedCategory
@ -91,6 +114,127 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi
return "Complex" return "Complex"
} }
const handleCreateTemplate = async (templateData: Partial<DatabaseTemplate>) => {
try {
await createTemplate(templateData)
setShowCustomForm(false)
} catch (error) {
console.error('Error creating template:', error)
}
}
const handleUpdateTemplate = async (id: string, templateData: Partial<DatabaseTemplate>) => {
try {
await updateTemplate(id, templateData)
setEditingTemplate(null)
} catch (error) {
console.error('Error updating template:', error)
}
}
const handleDeleteTemplate = async () => {
if (!deletingTemplate) return
setDeleteLoading(true)
try {
await deleteTemplate(deletingTemplate.id)
setDeletingTemplate(null)
} catch (error) {
console.error('Error deleting template:', error)
} finally {
setDeleteLoading(false)
}
}
// Show loading state
if (loading) {
return (
<div className="max-w-7xl mx-auto space-y-6">
<div className="text-center space-y-3">
<h1 className="text-4xl font-bold text-white">Loading Templates...</h1>
<p className="text-xl text-white/60">Fetching templates from database</p>
</div>
<div className="flex justify-center">
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-orange-500"></div>
</div>
</div>
)
}
// Show error state
if (error) {
return (
<div className="max-w-7xl mx-auto space-y-6">
<div className="text-center space-y-3">
<div className="flex items-center justify-center space-x-2 text-red-400">
<AlertCircle className="h-6 w-6" />
<h1 className="text-2xl font-bold">Error Loading Templates</h1>
</div>
<p className="text-white/60">{error}</p>
<Button onClick={() => window.location.reload()} className="bg-orange-500 hover:bg-orange-400 text-black">
Retry
</Button>
</div>
</div>
)
}
// Show custom template form
if (showCustomForm) {
return (
<div className="max-w-4xl mx-auto space-y-6">
<div className="text-center space-y-3">
<h1 className="text-4xl font-bold text-white">Create Custom Template</h1>
<p className="text-xl text-white/60">Design your own project template</p>
</div>
<CustomTemplateForm
onSubmit={handleCreateTemplate}
onCancel={() => setShowCustomForm(false)}
/>
</div>
)
}
// Show edit template form
if (editingTemplate) {
return (
<div className="max-w-4xl mx-auto space-y-6">
<div className="text-center space-y-3">
<h1 className="text-4xl font-bold text-white">Edit Template</h1>
<p className="text-xl text-white/60">Modify your template settings</p>
</div>
<EditTemplateForm
template={editingTemplate}
onSubmit={handleUpdateTemplate}
onCancel={() => setEditingTemplate(null)}
/>
</div>
)
}
// Show delete confirmation dialog
if (deletingTemplate) {
return (
<>
<div className="max-w-7xl mx-auto space-y-6">
{/* Header */}
<div className="text-center space-y-3">
<h1 className="text-4xl font-bold text-white">Choose Your Project Template</h1>
<p className="text-xl text-white/60 max-w-3xl mx-auto">
Select from our comprehensive library of professionally designed templates
</p>
</div>
</div>
<DeleteConfirmationDialog
templateTitle={deletingTemplate.title}
onConfirm={handleDeleteTemplate}
onCancel={() => setDeletingTemplate(null)}
loading={deleteLoading}
/>
</>
)
}
return ( return (
<div className="max-w-7xl mx-auto space-y-6"> <div className="max-w-7xl mx-auto space-y-6">
{/* Header */} {/* Header */}
@ -122,13 +266,13 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi
<button <button
key={category.id} key={category.id}
onClick={() => setSelectedCategory(category.id)} onClick={() => setSelectedCategory(category.id)}
className={`flex items-center space-x-3 px-6 py-3 rounded-xl border transition-all ${ className={`flex items-center space-x-3 px-6 py-3 rounded-xl border transition-all cursor-pointer ${
active active
? "bg-orange-500 text-black border-orange-500" ? "bg-orange-500 text-black border-orange-500"
: "bg-white/5 text-white/80 border-white/10 hover:bg-white/10" : "bg-white/5 text-white/80 border-white/10 hover:bg-white/10"
}`} }`}
> >
<div className={`p-2 rounded-lg ${active ? "bg-black" : "bg-white/10"}`}> <div className={`p-2 rounded-lg ${active ? "bg-white/10" : "bg-white/10"}`}>
<Icon className="h-5 w-5" /> <Icon className="h-5 w-5" />
</div> </div>
<div className="text-left"> <div className="text-left">
@ -163,6 +307,33 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi
)} )}
</div> </div>
</div> </div>
{/* Edit and Delete buttons - only show for database templates */}
{template.id && template.id !== "marketing-website" && template.id !== "saas-platform" && (
<div className="flex items-center space-x-1 ml-2">
<Button
size="sm"
variant="ghost"
onClick={(e) => {
e.stopPropagation()
setEditingTemplate(template as DatabaseTemplate)
}}
className="h-8 w-8 p-0 text-white/60 hover:text-white hover:bg-white/10 cursor-pointer"
>
<Edit className="h-4 w-4" />
</Button>
<Button
size="sm"
variant="ghost"
onClick={(e) => {
e.stopPropagation()
setDeletingTemplate(template as DatabaseTemplate)
}}
className="h-8 w-8 p-0 text-white/60 hover:text-red-400 hover:bg-red-400/10 cursor-pointer"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
)}
</div> </div>
<p className="text-white/80 text-sm leading-relaxed">{template.description}</p> <p className="text-white/80 text-sm leading-relaxed">{template.description}</p>
</div> </div>
@ -176,7 +347,7 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi
</div> </div>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Layers className="h-4 w-4 text-emerald-400" /> <Layers className="h-4 w-4 text-emerald-400" />
<span className="font-medium">{template.features.length} features</span> <span className="font-medium">{(template as any).featureCount ?? template.features.length} features</span>
</div> </div>
</div> </div>
@ -235,7 +406,7 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi
</div> </div>
{/* Custom Template Option */} {/* Custom Template Option */}
<Card className="group border-dashed border-2 border-white/15 bg-white/5 hover:border-white/25 transition-all"> <Card className="group border-dashed border-2 border-white/15 bg-white/5 hover:border-white/25 transition-all cursor-pointer" onClick={() => setShowCustomForm(true)}>
<CardContent className="text-center py-16 px-8 text-white/80"> <CardContent className="text-center py-16 px-8 text-white/80">
<div className="w-20 h-20 bg-white/10 rounded-full flex items-center justify-center mx-auto mb-6"> <div className="w-20 h-20 bg-white/10 rounded-full flex items-center justify-center mx-auto mb-6">
<Plus className="h-10 w-10 text-orange-400" /> <Plus className="h-10 w-10 text-orange-400" />
@ -270,44 +441,132 @@ function FeatureSelectionStep({
onNext, onNext,
onBack, onBack,
}: { template: Template; onNext: () => void; onBack: () => void }) { }: { template: Template; onNext: () => void; onBack: () => void }) {
return ( const { fetchFeatures, createFeature, updateFeature, deleteFeature } = useTemplates()
<div className="max-w-7xl mx-auto space-y-8"> const [features, setFeatures] = useState<TemplateFeature[]>([])
{/* Header */} const [loading, setLoading] = useState(true)
<div className="text-center space-y-4"> const [error, setError] = useState<string | null>(null)
<h1 className="text-4xl font-bold text-white">Select Features for {template.title}</h1> const [newFeature, setNewFeature] = useState({ name: '', description: '', complexity: 'medium' as 'low' | 'medium' | 'high' })
<p className="text-xl text-white/60 max-w-3xl mx-auto">
Choose the features that best fit your project requirements.
</p>
</div>
{/* Features List */} const load = async () => {
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8"> try {
{template.features.map((feature, index) => ( setLoading(true)
<Card key={index} className="hover:shadow-xl transition-all duration-300 cursor-pointer group border-2 hover:border-orange-200 bg-white/5"> const data = await fetchFeatures(template.id)
<CardHeader className="pb-4"> setFeatures(data)
<CardTitle className="text-xl group-hover:text-orange-400 transition-colors line-clamp-2 text-white"> } catch (e) {
{feature} setError(e instanceof Error ? e.message : 'Failed to load features')
} finally {
setLoading(false)
}
}
// Initial load
useEffect(() => { load() }, [template.id])
const handleAddCustom = async () => {
if (!newFeature.name.trim()) return
await createFeature(template.id, {
name: newFeature.name,
description: newFeature.description,
feature_type: 'custom',
complexity: newFeature.complexity,
is_default: false,
created_by_user: true,
})
setNewFeature({ name: '', description: '', complexity: 'medium' })
await load()
}
const handleUpdate = async (f: TemplateFeature, updates: Partial<TemplateFeature>) => {
await updateFeature(f.id, { ...updates, isCustom: f.feature_type === 'custom' })
await load()
}
const handleDelete = async (f: TemplateFeature) => {
await deleteFeature(f.id, { isCustom: f.feature_type === 'custom' })
await load()
}
const section = (title: string, list: TemplateFeature[]) => (
<div>
<h3 className="text-lg font-semibold text-white mb-3">{title} ({list.length})</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{list.map((f) => (
<Card key={f.id} className="bg-white/5 border-white/10">
<CardHeader className="pb-3">
<CardTitle className="text-white flex items-center justify-between">
<span>{f.name}</span>
{f.feature_type === 'custom' && (
<div className="flex gap-2">
<Button size="sm" variant="outline" className="border-white/20 text-white hover:bg-white/10" onClick={async () => {
const newName = window.prompt('Update feature name', f.name || '')
if (newName === null) return
const newDesc = window.prompt('Update description', f.description || '')
await handleUpdate(f, { name: newName, description: newDesc ?? f.description })
}}>Edit</Button>
<Button size="sm" variant="outline" className="border-red-500 text-red-300 hover:bg-red-500/10" onClick={() => handleDelete(f)}>Delete</Button>
</div>
)}
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent className="text-white/80 text-sm space-y-2">
<CardContent className="space-y-4 text-white/80"> <p>{f.description || 'No description provided.'}</p>
<p className="text-white/80 text-sm leading-relaxed"> <div className="flex gap-2 text-xs">
This feature enhances your project with {feature} capabilities. <Badge variant="outline" className="bg-white/5 border-white/10">{f.feature_type}</Badge>
</p> <Badge variant="outline" className="bg-white/5 border-white/10">{f.complexity}</Badge>
{typeof f.usage_count === 'number' && <Badge variant="outline" className="bg-white/5 border-white/10">used {f.usage_count}</Badge>}
</div>
</CardContent> </CardContent>
</Card> </Card>
))} ))}
</div> </div>
</div>
)
if (loading) {
return <div className="text-center py-20 text-white/60">Loading features...</div>
}
if (error) {
return <div className="text-center py-20 text-red-400">{error}</div>
}
const essentials = features.filter(f => f.feature_type === 'essential')
const suggested = features.filter(f => f.feature_type === 'suggested')
const custom = features.filter(f => f.feature_type === 'custom')
return (
<div className="max-w-7xl mx-auto space-y-8">
<div className="text-center space-y-4">
<h1 className="text-4xl font-bold text-white">Select Features for {template.title}</h1>
<p className="text-xl text-white/60 max-w-3xl mx-auto">Choose defaults or add your own custom features.</p>
</div>
{section('Essential Features', essentials)}
{section('Suggested Features', suggested)}
{/* Add custom feature */}
<div className="bg-white/5 border border-white/10 rounded-xl p-4 space-y-3">
<h3 className="text-white font-semibold">Add Custom Feature</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
<Input placeholder="Feature name" value={newFeature.name} onChange={(e) => setNewFeature({ ...newFeature, name: e.target.value })} className="bg-white/5 border-white/10 text-white placeholder:text-white/40" />
<Input placeholder="Description (optional)" value={newFeature.description} onChange={(e) => setNewFeature({ ...newFeature, description: e.target.value })} className="bg-white/5 border-white/10 text-white placeholder:text-white/40" />
<select value={newFeature.complexity} onChange={(e) => setNewFeature({ ...newFeature, complexity: e.target.value as any })} className="bg-white/5 border-white/10 text-white rounded-md px-3">
<option value="low">low</option>
<option value="medium">medium</option>
<option value="high">high</option>
</select>
</div>
<div className="flex justify-between items-center">
<div className="text-white/60 text-sm">Custom features are saved to this template.</div>
<Button onClick={handleAddCustom} className="bg-orange-500 hover:bg-orange-400 text-black">Add Feature</Button>
</div>
</div>
{section('Your Custom Features', custom)}
{/* Navigation Buttons */}
<div className="text-center py-4"> <div className="text-center py-4">
<div className="space-x-4"> <div className="space-x-4">
<Button variant="outline" onClick={onBack} className="border-white/20 text-white hover:bg-white/10"> <Button variant="outline" onClick={onBack} className="border-white/20 text-white hover:bg-white/10">Back</Button>
Back <Button onClick={onNext} className="bg-orange-500 hover:bg-orange-400 text-black font-semibold py-2 rounded-lg shadow">Continue</Button>
</Button>
<Button onClick={onNext} className="bg-orange-500 hover:bg-orange-400 text-black font-semibold py-2 rounded-lg shadow">
Continue
</Button>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,10 +1,10 @@
"use client" "use client";
import { useState } from "react" import { useState } from "react";
import Link from "next/link" import Link from "next/link";
import { usePathname } from "next/navigation" import { usePathname } from "next/navigation";
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
@ -12,10 +12,10 @@ import {
DropdownMenuLabel, DropdownMenuLabel,
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu" } from "@/components/ui/dropdown-menu";
import { Badge } from "@/components/ui/badge" import { Badge } from "@/components/ui/badge";
import { Bell, Settings, LogOut, User, Menu, X } from "lucide-react" import { Bell, Settings, LogOut, User, Menu, X } from "lucide-react";
import { useAuth } from "@/contexts/auth-context" import { useAuth } from "@/contexts/auth-context";
const navigation = [ const navigation = [
{ name: "Project Builder", href: "/", current: false }, { name: "Project Builder", href: "/", current: false },
@ -23,12 +23,30 @@ const navigation = [
{ name: "Features", href: "/features", current: false }, { name: "Features", href: "/features", current: false },
{ name: "Business Context", href: "/business-context", current: false }, { name: "Business Context", href: "/business-context", current: false },
{ name: "Architecture", href: "/architecture", current: false }, { name: "Architecture", href: "/architecture", current: false },
] ];
export default function Header() { export default function Header() {
const [mobileMenuOpen, setMobileMenuOpen] = useState(false) const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const pathname = usePathname() const [isLoggingOut, setIsLoggingOut] = useState(false);
const { user, logout } = useAuth() const pathname = usePathname();
const { user, logout, isLoading } = useAuth();
// Handle logout with loading state
const handleLogout = async () => {
try {
setIsLoggingOut(true);
await logout();
} catch (error) {
console.error('Logout failed:', error);
setIsLoggingOut(false);
}
};
// Define routes where the header should not be shown
const noHeaderRoutes = ["/signin", "/signup"];
if (noHeaderRoutes.includes(pathname)) {
return null;
}
return ( return (
<header className="bg-black/80 text-white border-b border-white/10 backdrop-blur"> <header className="bg-black/80 text-white border-b border-white/10 backdrop-blur">
@ -63,9 +81,9 @@ export default function Header() {
{/* Right side */} {/* Right side */}
<div className="flex items-center space-x-4 cursor-pointer"> <div className="flex items-center space-x-4 cursor-pointer">
{/* Auth Button or User Menu */} {/* While loading, don't show sign-in or user menu to avoid flicker */}
{!user ? ( {!isLoading && (!user ? (
<Link href="/auth"> <Link href="/signin">
<Button size="sm" className="cursor-pointer">Sign In</Button> <Button size="sm" className="cursor-pointer">Sign In</Button>
</Link> </Link>
) : ( ) : (
@ -78,24 +96,26 @@ export default function Header() {
</Badge> </Badge>
</Button> </Button>
</> </>
)} ))}
{/* User Menu - Only show when user is authenticated */} {/* User Menu - Only show when user is authenticated */}
{user && ( {!isLoading && user && user.email && (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button variant="ghost" className="relative h-8 w-8 rounded-full cursor-pointer hover:bg-white/5"> <Button variant="ghost" className="relative h-8 w-8 rounded-full cursor-pointer hover:bg-white/5">
<Avatar className="h-8 w-8"> <Avatar className="h-8 w-8">
<AvatarImage src={user.avatar || "/avatars/01.png"} alt={user.name} /> <AvatarImage src={user.avatar || "/avatars/01.png"} alt={user.name || user.email || "User"} />
<AvatarFallback>{user.name.charAt(0).toUpperCase()}</AvatarFallback> <AvatarFallback>
{(user.name && user.name.charAt(0)) || (user.email && user.email.charAt(0)) || "U"}
</AvatarFallback>
</Avatar> </Avatar>
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent className="w-56 bg-black text-white border-white/10" align="end" forceMount> <DropdownMenuContent className="w-56 bg-black text-white border-white/10" align="end" forceMount>
<DropdownMenuLabel className="font-normal"> <DropdownMenuLabel className="font-normal">
<div className="flex flex-col space-y-1"> <div className="flex flex-col space-y-1">
<p className="text-sm font-medium leading-none">{user.name}</p> <p className="text-sm font-medium leading-none">{user.name || user.email || "User"}</p>
<p className="text-xs leading-none text-white/60">{user.email}</p> <p className="text-xs leading-none text-white/60">{user.email || "No email"}</p>
</div> </div>
</DropdownMenuLabel> </DropdownMenuLabel>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
@ -108,9 +128,9 @@ export default function Header() {
<span>Settings</span> <span>Settings</span>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem onClick={logout} className="hover:bg-white/5 cursor-pointer"> <DropdownMenuItem onClick={handleLogout} className="hover:bg-white/5 cursor-pointer" disabled={isLoggingOut}>
<LogOut className="mr-2 h-4 w-4" /> <LogOut className="mr-2 h-4 w-4" />
<span>Log out</span> <span>{isLoggingOut ? 'Logging out...' : 'Log out'}</span>
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
@ -146,5 +166,5 @@ export default function Header() {
)} )}
</div> </div>
</header> </header>
) );
} }

View File

@ -0,0 +1,24 @@
import * as React from "react"
import { cn } from "@/lib/utils"
export interface TextareaProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Textarea.displayName = "Textarea"
export { Textarea }

View File

@ -1,6 +1,8 @@
"use client" "use client"
import React, { createContext, useContext, useState, useEffect, ReactNode } from "react" import React, { createContext, useContext, useState, useEffect, ReactNode } from "react"
import { logout as apiLogout } from "@/components/apis/authApiClients"
import { safeLocalStorage } from "@/lib/utils"
interface User { interface User {
id: string id: string
@ -15,7 +17,8 @@ interface AuthContextType {
isLoading: boolean isLoading: boolean
login: (email: string, password: string) => Promise<boolean> login: (email: string, password: string) => Promise<boolean>
signup: (name: string, email: string, password: string) => Promise<boolean> signup: (name: string, email: string, password: string) => Promise<boolean>
logout: () => void logout: () => Promise<void>
setUserFromApi: (apiUser: any) => void
} }
const AuthContext = createContext<AuthContextType | undefined>(undefined) const AuthContext = createContext<AuthContextType | undefined>(undefined)
@ -40,14 +43,21 @@ export function AuthProvider({ children }: AuthProviderProps) {
useEffect(() => { useEffect(() => {
const checkAuth = () => { const checkAuth = () => {
// Check localStorage for user data // Check localStorage for user data
const userData = localStorage.getItem("codenuk_user") const userData = safeLocalStorage.getItem("codenuk_user")
if (userData) { if (userData) {
try { try {
const user = JSON.parse(userData) const user = JSON.parse(userData)
setUser(user) // Ensure user object has all required properties with fallbacks
const validatedUser: User = {
id: user.id || "1",
name: user.name || user.email?.split("@")[0] || "User",
email: user.email || "user@example.com",
avatar: user.avatar || "/avatars/01.png"
}
setUser(validatedUser)
} catch (error) { } catch (error) {
console.error("Error parsing user data:", error) console.error("Error parsing user data:", error)
localStorage.removeItem("codenuk_user") safeLocalStorage.removeItem("codenuk_user")
} }
} }
setIsLoading(false) setIsLoading(false)
@ -56,6 +66,22 @@ export function AuthProvider({ children }: AuthProviderProps) {
checkAuth() checkAuth()
}, []) }, [])
// Allow setting user right after successful API login
const setUserFromApi = (apiUser: any) => {
const validatedUser: User = {
id: (apiUser.id || apiUser.user_id || "1").toString(),
name:
apiUser.name ||
[apiUser.first_name, apiUser.last_name].filter(Boolean).join(" ") ||
apiUser.username ||
(apiUser.email ? apiUser.email.split("@")[0] : "User"),
email: apiUser.email || "user@example.com",
avatar: apiUser.avatar || "/avatars/01.png",
}
setUser(validatedUser)
safeLocalStorage.setItem("codenuk_user", JSON.stringify(validatedUser))
}
const login = async (email: string, password: string): Promise<boolean> => { const login = async (email: string, password: string): Promise<boolean> => {
try { try {
setIsLoading(true) setIsLoading(true)
@ -73,7 +99,7 @@ export function AuthProvider({ children }: AuthProviderProps) {
} }
setUser(user) setUser(user)
localStorage.setItem("codenuk_user", JSON.stringify(user)) safeLocalStorage.setItem("codenuk_user", JSON.stringify(user))
return true return true
} }
@ -103,7 +129,7 @@ export function AuthProvider({ children }: AuthProviderProps) {
} }
setUser(user) setUser(user)
localStorage.setItem("codenuk_user", JSON.stringify(user)) safeLocalStorage.setItem("codenuk_user", JSON.stringify(user))
return true return true
} }
@ -116,9 +142,16 @@ export function AuthProvider({ children }: AuthProviderProps) {
} }
} }
const logout = () => { const logout = async (): Promise<void> => {
setUser(null) try {
localStorage.removeItem("codenuk_user") // Call the API logout function which handles backend call, token clearing, and redirect
await apiLogout();
} catch (error) {
console.error("Logout error:", error);
// Even if there's an error, clear local state
setUser(null);
safeLocalStorage.removeItem("codenuk_user");
}
} }
const value: AuthContextType = { const value: AuthContextType = {
@ -127,7 +160,8 @@ export function AuthProvider({ children }: AuthProviderProps) {
isLoading, isLoading,
login, login,
signup, signup,
logout logout,
setUserFromApi
} }
return ( return (

138
src/hooks/useTemplates.ts Normal file
View File

@ -0,0 +1,138 @@
import { useState, useEffect } from 'react'
import { templateService, DatabaseTemplate, TemplatesByCategory, TemplateFeature } from '@/lib/template-service'
export function useTemplates() {
const [templates, setTemplates] = useState<TemplatesByCategory>({})
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const fetchTemplates = async () => {
try {
setLoading(true)
setError(null)
const data = await templateService.getTemplatesByCategory()
setTemplates(data)
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to fetch templates')
console.error('Error fetching templates:', err)
} finally {
setLoading(false)
}
}
useEffect(() => {
fetchTemplates()
}, [])
const createTemplate = async (templateData: Partial<DatabaseTemplate>) => {
try {
const newTemplate = await templateService.createTemplate(templateData)
// Refresh templates after creating a new one
await fetchTemplates()
return newTemplate
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to create template')
throw err
}
}
const updateTemplate = async (id: string, templateData: Partial<DatabaseTemplate>) => {
try {
const updatedTemplate = await templateService.updateTemplate(id, templateData)
// Refresh templates after updating
await fetchTemplates()
return updatedTemplate
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to update template')
throw err
}
}
const deleteTemplate = async (id: string) => {
try {
await templateService.deleteTemplate(id)
// Refresh templates after deleting
await fetchTemplates()
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to delete template')
throw err
}
}
// Convert database templates to the format expected by the UI
const getTemplatesForUI = () => {
const allTemplates: Array<DatabaseTemplate & {
features: string[]
complexity: number
timeEstimate: string
techStack: string[]
popularity?: number
lastUpdated?: string
}> = []
Object.entries(templates).forEach(([category, categoryTemplates]) => {
categoryTemplates.forEach((template) => {
// Convert database template to UI format
const uiTemplate = {
...template,
features: [], // Will be populated when template is selected
complexity: 3, // Default complexity
timeEstimate: "2-4 weeks", // Default time estimate
techStack: ["Next.js", "PostgreSQL", "Tailwind CSS"], // Default tech stack
popularity: template.avg_rating ? Math.round(template.avg_rating * 20) : 75, // Convert rating to popularity
lastUpdated: template.updated_at ? new Date(template.updated_at).toISOString().split('T')[0] : undefined,
featureCount: (template as any).feature_count ?? 0
}
allTemplates.push(uiTemplate)
})
})
return allTemplates
}
// Get template features when a template is selected
const getTemplateFeatures = async (templateId: string): Promise<string[]> => {
try {
const features = await templateService.getFeaturesForTemplate(templateId)
return features.map(feature => feature.name)
} catch (error) {
console.error('Error fetching template features:', error)
return []
}
}
// Feature CRUD for a template
const fetchFeatures = async (templateId: string): Promise<TemplateFeature[]> => {
return templateService.getFeaturesForTemplate(templateId)
}
const createFeature = async (templateId: string, feature: Partial<TemplateFeature>) => {
const payload = { ...feature, template_id: templateId }
return templateService.createFeature(payload)
}
const updateFeature = async (featureId: string, updates: Partial<TemplateFeature>) => {
// If updates indicate custom, route accordingly
return templateService.updateFeature(featureId, updates)
}
const deleteFeature = async (featureId: string, opts?: { isCustom?: boolean }) => {
return templateService.deleteFeature(featureId, opts)
}
return {
templates,
loading,
error,
fetchTemplates,
createTemplate,
updateTemplate,
deleteTemplate,
getTemplatesForUI,
getTemplateFeatures,
fetchFeatures,
createFeature,
updateFeature,
deleteFeature
}
}

232
src/lib/template-service.ts Normal file
View File

@ -0,0 +1,232 @@
export interface DatabaseTemplate {
id: string
type: string
title: string
description: string
icon: string | null
category: string
gradient: string | null
border: string | null
text: string | null
subtext: string | null
is_active: boolean
created_at: string
updated_at: string
feature_count?: number
avg_rating?: number
}
export interface TemplateFeature {
id: string
template_id: string
feature_id: string
name: string
description: string
feature_type: 'essential' | 'suggested' | 'custom'
complexity: 'low' | 'medium' | 'high'
display_order: number
usage_count: number
user_rating: number
is_default: boolean
created_by_user: boolean
created_at: string
updated_at: string
}
export interface TemplateWithFeatures extends DatabaseTemplate {
features: TemplateFeature[]
}
export interface TemplatesByCategory {
[category: string]: DatabaseTemplate[]
}
const API_BASE_URL = 'http://localhost:8009'
class TemplateService {
private async makeRequest<T>(endpoint: string): Promise<T> {
try {
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
})
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const data = await response.json()
if (!data.success) {
throw new Error(data.message || 'API request failed')
}
return data.data
} catch (error) {
console.error('Template service error:', error)
throw error
}
}
async getTemplatesByCategory(): Promise<TemplatesByCategory> {
return this.makeRequest<TemplatesByCategory>('/api/templates')
}
async getTemplateById(id: string): Promise<TemplateWithFeatures> {
return this.makeRequest<TemplateWithFeatures>(`/api/templates/${id}`)
}
async getTemplateByType(type: string): Promise<TemplateWithFeatures> {
return this.makeRequest<TemplateWithFeatures>(`/api/templates/type/${type}`)
}
async createTemplate(templateData: Partial<DatabaseTemplate>): Promise<DatabaseTemplate> {
try {
const response = await fetch(`${API_BASE_URL}/api/templates`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(templateData),
})
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const data = await response.json()
if (!data.success) {
throw new Error(data.message || 'Failed to create template')
}
return data.data
} catch (error) {
console.error('Create template error:', error)
throw error
}
}
// Features API
async getFeaturesForTemplate(templateId: string): Promise<TemplateFeature[]> {
// Use merged endpoint to include custom features
try {
return await this.makeRequest<TemplateFeature[]>(`/api/features/templates/${templateId}/merged`)
} catch {
// Fallback to default-only if merged endpoint unsupported
return this.makeRequest<TemplateFeature[]>(`/api/templates/${templateId}/features`)
}
}
async searchFeatures(searchTerm: string, templateId?: string): Promise<TemplateFeature[]> {
const q = encodeURIComponent(searchTerm)
const extra = templateId ? `&template_id=${encodeURIComponent(templateId)}` : ''
return this.makeRequest<TemplateFeature[]>(`/api/features/search?q=${q}${extra}`)
}
async createFeature(featureData: Partial<TemplateFeature>): Promise<TemplateFeature> {
if (
featureData &&
(featureData.feature_type === 'custom' || (featureData as any).feature_type === 'custom')
) {
const response = await fetch(`${API_BASE_URL}/api/features/custom`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(featureData),
})
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`)
const data = await response.json()
if (!data.success) throw new Error(data.message || 'Failed to create custom feature')
return data.data
}
const response = await fetch(`${API_BASE_URL}/api/features`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(featureData),
})
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`)
const data = await response.json()
if (!data.success) throw new Error(data.message || 'Failed to create feature')
return data.data
}
async updateFeature(id: string, featureData: Partial<TemplateFeature> & { isCustom?: boolean }): Promise<TemplateFeature> {
const isCustom = (featureData as any).isCustom || featureData.feature_type === 'custom'
const url = isCustom ? `${API_BASE_URL}/api/features/custom/${id}` : `${API_BASE_URL}/api/features/${id}`
const response = await fetch(url, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(featureData),
})
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`)
const data = await response.json()
if (!data.success) throw new Error(data.message || 'Failed to update feature')
return data.data
}
async deleteFeature(id: string, opts?: { isCustom?: boolean }): Promise<void> {
const url = opts?.isCustom ? `${API_BASE_URL}/api/features/custom/${id}` : `${API_BASE_URL}/api/features/${id}`
const response = await fetch(url, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
})
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`)
const data = await response.json()
if (!data.success) throw new Error(data.message || 'Failed to delete feature')
}
async updateTemplate(id: string, templateData: Partial<DatabaseTemplate>): Promise<DatabaseTemplate> {
try {
const response = await fetch(`${API_BASE_URL}/api/templates/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(templateData),
})
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const data = await response.json()
if (!data.success) {
throw new Error(data.message || 'Failed to update template')
}
return data.data
} catch (error) {
console.error('Update template error:', error)
throw error
}
}
async deleteTemplate(id: string): Promise<void> {
try {
const response = await fetch(`${API_BASE_URL}/api/templates/${id}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
})
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const data = await response.json()
if (!data.success) {
throw new Error(data.message || 'Failed to delete template')
}
} catch (error) {
console.error('Delete template error:', error)
throw error
}
}
}
export const templateService = new TemplateService()

View File

@ -1,6 +1,33 @@
import { clsx, type ClassValue } from "clsx" import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge" import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)) return twMerge(clsx(inputs))
} }
// Safe redirect function that works in both client and server environments
export const safeRedirect = (url: string) => {
if (typeof window !== 'undefined') {
window.location.href = url;
}
};
// Safe localStorage functions
export const safeLocalStorage = {
getItem: (key: string): string | null => {
if (typeof window !== 'undefined' && window.localStorage) {
return window.localStorage.getItem(key);
}
return null;
},
setItem: (key: string, value: string): void => {
if (typeof window !== 'undefined' && window.localStorage) {
window.localStorage.setItem(key, value);
}
},
removeItem: (key: string): void => {
if (typeof window !== 'undefined' && window.localStorage) {
window.localStorage.removeItem(key);
}
}
};