From 722f878243a59b517f99a1ab5ae12f9c08d1d09e Mon Sep 17 00:00:00 2001 From: "tejas.prakash" Date: Wed, 20 Aug 2025 12:42:57 +0530 Subject: [PATCH] Changes done in user-auth and template --- AUTH_IMPLEMENTATION.md | 231 ++++++++++ DYNAMIC_TEMPLATES.md | 118 +++++ package-lock.json | 116 ++++- package.json | 1 + src/app/auth/emailVerification.tsx | 170 +++++++ src/app/auth/page.tsx | 32 +- src/app/signin/page.tsx | 5 + src/app/signup/page.tsx | 5 + src/app/verify-email/page.tsx | 5 + src/components/apis/authApiClients.tsx | 96 ++++ src/components/apis/authenticationHandler.tsx | 75 ++++ src/components/auth/emailVerification.tsx | 6 + src/components/auth/signin-form.tsx | 71 ++- src/components/auth/signin-page.tsx | 124 ++++++ src/components/auth/signup-form.tsx | 323 +++++++++----- src/components/auth/signup-page.tsx | 157 +++++++ src/components/custom-template-form.tsx | 210 +++++++++ src/components/delete-confirmation-dialog.tsx | 60 +++ src/components/edit-template-form.tsx | 225 ++++++++++ src/components/layout/app-layout.tsx | 4 +- src/components/main-dashboard.tsx | 413 ++++++++++++++---- src/components/navigation/header.tsx | 74 ++-- src/components/ui/textarea.tsx | 24 + src/contexts/auth-context.tsx | 54 ++- src/hooks/useTemplates.ts | 138 ++++++ src/lib/template-service.ts | 232 ++++++++++ src/lib/utils.ts | 29 +- 27 files changed, 2726 insertions(+), 272 deletions(-) create mode 100644 AUTH_IMPLEMENTATION.md create mode 100644 DYNAMIC_TEMPLATES.md create mode 100644 src/app/auth/emailVerification.tsx create mode 100644 src/app/signin/page.tsx create mode 100644 src/app/signup/page.tsx create mode 100644 src/app/verify-email/page.tsx create mode 100644 src/components/apis/authApiClients.tsx create mode 100644 src/components/apis/authenticationHandler.tsx create mode 100644 src/components/auth/emailVerification.tsx create mode 100644 src/components/auth/signin-page.tsx create mode 100644 src/components/auth/signup-page.tsx create mode 100644 src/components/custom-template-form.tsx create mode 100644 src/components/delete-confirmation-dialog.tsx create mode 100644 src/components/edit-template-form.tsx create mode 100644 src/components/ui/textarea.tsx create mode 100644 src/hooks/useTemplates.ts create mode 100644 src/lib/template-service.ts diff --git a/AUTH_IMPLEMENTATION.md b/AUTH_IMPLEMENTATION.md new file mode 100644 index 0000000..5bd1e0a --- /dev/null +++ b/AUTH_IMPLEMENTATION.md @@ -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=` +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 +loginUser(email: string, password: string): Promise +``` + +## 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** diff --git a/DYNAMIC_TEMPLATES.md b/DYNAMIC_TEMPLATES.md new file mode 100644 index 0000000..63eb0bd --- /dev/null +++ b/DYNAMIC_TEMPLATES.md @@ -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 diff --git a/package-lock.json b/package-lock.json index 7b1fefa..e621bda 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "@radix-ui/react-select": "^2.2.5", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tabs": "^1.1.12", + "axios": "^1.11.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.539.0", @@ -3012,6 +3013,12 @@ "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": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -3038,6 +3045,17 @@ "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": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -3102,7 +3120,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -3258,6 +3275,18 @@ "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": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -3409,6 +3438,15 @@ "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": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", @@ -3442,7 +3480,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -3547,7 +3584,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -3557,7 +3593,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -3595,7 +3630,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -3608,7 +3642,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -4215,6 +4248,26 @@ "dev": true, "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": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -4231,11 +4284,26 @@ "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": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -4276,7 +4344,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -4310,7 +4377,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -4398,7 +4464,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -4477,7 +4542,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -4490,7 +4554,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -4506,7 +4569,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -5429,7 +5491,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -5459,6 +5520,27 @@ "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": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -5960,6 +6042,12 @@ "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": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", diff --git a/package.json b/package.json index 365a91b..82b3ceb 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@radix-ui/react-select": "^2.2.5", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tabs": "^1.1.12", + "axios": "^1.11.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.539.0", diff --git a/src/app/auth/emailVerification.tsx b/src/app/auth/emailVerification.tsx new file mode 100644 index 0000000..37b2313 --- /dev/null +++ b/src/app/auth/emailVerification.tsx @@ -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("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 ( +
+
+
+

Verifying Email

+

{message}

+
+
+ ); + } + + return ( +
+
+ {status === "success" && ( +
+

{message || "Email verified. Redirecting..."}

+
+ )} + + {status === "error" && ( +
+

{error}

+ +
+ )} + +
+ + +
+
+
+ ); +}; + +export default EmailVerification; \ No newline at end of file diff --git a/src/app/auth/page.tsx b/src/app/auth/page.tsx index a1d510f..57f3440 100644 --- a/src/app/auth/page.tsx +++ b/src/app/auth/page.tsx @@ -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() { - return + 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 ( +
+
+
+

Redirecting...

+
+
+ ) } diff --git a/src/app/signin/page.tsx b/src/app/signin/page.tsx new file mode 100644 index 0000000..03bd6a4 --- /dev/null +++ b/src/app/signin/page.tsx @@ -0,0 +1,5 @@ +import { SignInPage } from "@/components/auth/signin-page" + +export default function SignInPageRoute() { + return +} diff --git a/src/app/signup/page.tsx b/src/app/signup/page.tsx new file mode 100644 index 0000000..1cb1743 --- /dev/null +++ b/src/app/signup/page.tsx @@ -0,0 +1,5 @@ +import { SignUpPage } from "@/components/auth/signup-page" + +export default function SignUpPageRoute() { + return +} diff --git a/src/app/verify-email/page.tsx b/src/app/verify-email/page.tsx new file mode 100644 index 0000000..f6ae03c --- /dev/null +++ b/src/app/verify-email/page.tsx @@ -0,0 +1,5 @@ +import EmailVerification from "@/app/auth/emailVerification"; + +export default function VerifyEmailPage() { + return ; +} diff --git a/src/components/apis/authApiClients.tsx b/src/components/apis/authApiClients.tsx new file mode 100644 index 0000000..6505b4c --- /dev/null +++ b/src/components/apis/authApiClients.tsx @@ -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); \ No newline at end of file diff --git a/src/components/apis/authenticationHandler.tsx b/src/components/apis/authenticationHandler.tsx new file mode 100644 index 0000000..9ffac8f --- /dev/null +++ b/src/components/apis/authenticationHandler.tsx @@ -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; + } +} \ No newline at end of file diff --git a/src/components/auth/emailVerification.tsx b/src/components/auth/emailVerification.tsx new file mode 100644 index 0000000..fe70dc9 --- /dev/null +++ b/src/components/auth/emailVerification.tsx @@ -0,0 +1,6 @@ +import EmailVerification from "@/app/auth/emailVerification"; + +export default function VerifyEmailPage() { + return ; +} + diff --git a/src/components/auth/signin-form.tsx b/src/components/auth/signin-form.tsx index e15d5bf..1154f34 100644 --- a/src/components/auth/signin-form.tsx +++ b/src/components/auth/signin-form.tsx @@ -7,14 +7,12 @@ import { useRouter } from "next/navigation" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" 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 { loginUser } from "@/components/apis/authenticationHandler" +import { setTokens } from "@/components/apis/authApiClients" -interface SignInFormProps { - onToggleMode: () => void -} - -export function SignInForm({ onToggleMode }: SignInFormProps) { +export function SignInForm() { const [showPassword, setShowPassword] = useState(false) const [isLoading, setIsLoading] = useState(false) const [error, setError] = useState("") @@ -23,23 +21,46 @@ export function SignInForm({ onToggleMode }: SignInFormProps) { password: "", }) - const { login } = useAuth() const router = useRouter() + const { setUserFromApi } = useAuth() const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() setError("") - setIsLoading(true) + if (!formData.email || !formData.password) { + setError("Please fill in all required fields.") + return + } + + setIsLoading(true) try { - const success = await login(formData.email, formData.password) - if (success) { + const response = await loginUser(formData.email, formData.password) + + 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("/") } 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 { setIsLoading(false) } @@ -47,7 +68,7 @@ export function SignInForm({ onToggleMode }: SignInFormProps) { return (
-
+
setFormData({ ...formData, email: 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" + 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" />
@@ -70,13 +91,13 @@ export function SignInForm({ onToggleMode }: SignInFormProps) { value={formData.password} onChange={(e) => setFormData({ ...formData, password: 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" + 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" /> -
- -
) diff --git a/src/components/auth/signin-page.tsx b/src/components/auth/signin-page.tsx new file mode 100644 index 0000000..173d1dc --- /dev/null +++ b/src/components/auth/signin-page.tsx @@ -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(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 ( +
+
+ {/* Left: Gradient panel with steps */} +
+
+ {/* subtle grain */} +
+
+
+ + {/* soft circular accents */} +
+
+
+ +
+
+

Codenuk

+
+

Welcome Back!

+

Sign in to access your workspace and continue building.

+ +
+ {/* Step 1 - Completed */} +
+
1
+ Sign up your account +
+ {/* Step 2 - Active */} +
+
2
+ Sign in to workspace +
+
+
+
+
+ + {/* Right: Form area */} +
+
+
+

Sign In Account

+

Enter your credentials to access your account.

+
+ + {/* Verification Message */} + {verificationMessage && ( +
+
+ {verificationType === 'success' ? ( + + ) : ( + + )} + {verificationMessage} +
+
+ )} + + + +
+
Don't have an account?
+ +
+
+
+
+
+ ) +} diff --git a/src/components/auth/signup-form.tsx b/src/components/auth/signup-form.tsx index 5a31652..8ef6d11 100644 --- a/src/components/auth/signup-form.tsx +++ b/src/components/auth/signup-form.tsx @@ -8,23 +8,27 @@ import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" 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 { registerUser } from "../apis/authenticationHandler" interface SignUpFormProps { - onToggleMode: () => void + onSignUpSuccess?: () => void } -export function SignUpForm({ onToggleMode }: SignUpFormProps) { +export function SignUpForm({ onSignUpSuccess }: SignUpFormProps) { const [showPassword, setShowPassword] = useState(false) const [showConfirmPassword, setShowConfirmPassword] = useState(false) const [isLoading, setIsLoading] = useState(false) const [error, setError] = useState("") const [formData, setFormData] = useState({ - name: "", + username: "", + first_name: "", + last_name: "", email: "", password: "", confirmPassword: "", + role: "user" // default role, adjust as needed }) const { signup } = useAuth() @@ -34,139 +38,228 @@ export function SignUpForm({ onToggleMode }: SignUpFormProps) { e.preventDefault() setError("") + // Validation if (formData.password !== formData.confirmPassword) { setError("Passwords don't match") 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) - try { - const success = await signup(formData.name, formData.email, formData.password) - if (success) { - router.push("/") + const response = await registerUser({ + username: formData.username, + 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 { + // Default behavior - redirect to signin with message + router.push("/signin?message=Account created successfully! Please check your email to verify your account.") + } } else { - setError("Failed to create account. Please try again.") + 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 { setIsLoading(false) } } return ( - - - Sign Up - Create your account to get started - - -
-
- - setFormData({ ...formData, name: 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" - /> +
+ + {/* Personal Information Section */} +
+
+ +

Personal Information

-
- + +
+
+ + setFormData({ ...formData, first_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" + /> +
+
+ + 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" + /> +
+
+ +
+
+ + 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" + /> +
+
+ + 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" + /> +
+
+
+ + {/* Contact Information Section */} +
+
+ +

Contact Information

+
+ +
+ setFormData({ ...formData, email: 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" + 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" />
-
- -
- setFormData({ ...formData, password: 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" - /> - -
-
-
- -
- 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" - /> - -
-
- - {error && ( -
- {error} -
- )} - - -
- -
- - - - ) -} +
+ {/* Security Section */} +
+
+ +

Security

+
+ +
+
+ +
+ setFormData({ ...formData, password: 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" + /> + +
+
+ +
+ +
+ 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" + /> + +
+
+
+
+ + {error && ( +
+
+ + {error} +
+
+ )} + + + +
+ ) +} \ No newline at end of file diff --git a/src/components/auth/signup-page.tsx b/src/components/auth/signup-page.tsx new file mode 100644 index 0000000..37e4530 --- /dev/null +++ b/src/components/auth/signup-page.tsx @@ -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 ( +
+
+ {/* Left: Gradient panel */} +
+
+ {/* subtle grain */} +
+
+
+ + {/* soft circular accents */} +
+
+
+ +
+
+

Codenuk

+
+

Account Created!

+

Your account has been successfully created. Please verify your email to continue.

+ +
+ {/* Step 1 - Completed */} +
+
+ +
+ Sign up completed +
+ {/* Step 2 - Next */} +
+
2
+ Verify email & sign in +
+
+
+
+
+ + {/* Right: Success message */} +
+
+
+
+ +
+

Account Created Successfully!

+

We've sent a verification email to your inbox. Please check your email and click the verification link to activate your account.

+
+

+ Next step: Check your email and click the verification link, then sign in to your account. +

+
+ +
+
+
+
+
+ ) + } + + return ( +
+
+ {/* Left: Gradient panel with steps */} +
+
+ {/* subtle grain */} +
+
+
+ + {/* soft circular accents */} +
+
+
+ +
+
+

Codenuk

+
+

Get Started with Us

+

Complete these easy steps to register your account.

+ +
+ {/* Step 1 - Active */} +
+
1
+ Sign up your account +
+ {/* Step 2 - Next */} +
+
2
+ Verify email & sign in +
+
+
+
+
+ + {/* Right: Form area */} +
+
+
+

Sign Up Account

+

Enter your personal data to create your account.

+
+ + + +
+
Already have an account?
+ +
+
+
+
+
+ ) +} diff --git a/src/components/custom-template-form.tsx b/src/components/custom-template-form.tsx new file mode 100644 index 0000000..5a4ce7d --- /dev/null +++ b/src/components/custom-template-form.tsx @@ -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) => Promise + 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 ( + + + + + Create Custom Template + + + +
+
+
+ + handleInputChange('type', e.target.value)} + className="bg-white/5 border-white/10 text-white placeholder:text-white/40" + required + /> +

Unique identifier for the template

+
+ +
+ + handleInputChange('title', e.target.value)} + className="bg-white/5 border-white/10 text-white placeholder:text-white/40" + required + /> +
+
+ +
+ +