initial commit
This commit is contained in:
16
.claude/settings.local.json
Normal file
16
.claude/settings.local.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(yarn:*)",
|
||||
"Bash(mkdir:*)",
|
||||
"Bash(ls:*)",
|
||||
"Bash(touch:*)",
|
||||
"Bash(NODE_ENV=test yarn build)",
|
||||
"Bash(npx tsc:*)",
|
||||
"Bash(yarn audit)",
|
||||
"Bash(npm audit:*)",
|
||||
"Bash(find:*)"
|
||||
],
|
||||
"deny": []
|
||||
}
|
||||
}
|
||||
8
.dockerignore
Normal file
8
.dockerignore
Normal file
@@ -0,0 +1,8 @@
|
||||
node_modules
|
||||
.next
|
||||
Dockerfile
|
||||
docker-compose*
|
||||
.git
|
||||
.gitignore
|
||||
.env*
|
||||
README.md
|
||||
10
.editorconfig
Normal file
10
.editorconfig
Normal file
@@ -0,0 +1,10 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
|
||||
[*.{js,json,yml}]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
4
.gitattributes
vendored
Normal file
4
.gitattributes
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
/.yarn/** linguist-vendored
|
||||
/.yarn/releases/* binary
|
||||
/.yarn/plugins/**/* binary
|
||||
/.pnp.* binary linguist-generated
|
||||
156
.github/workflows/deploy.yml
vendored
Normal file
156
.github/workflows/deploy.yml
vendored
Normal file
@@ -0,0 +1,156 @@
|
||||
name: Deploy SiliconPin
|
||||
on:
|
||||
push:
|
||||
branches: [ main ] # Change this to match your main branch name if different
|
||||
workflow_dispatch: # Allows manual workflow runs
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Cache Yarn dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cache/yarn
|
||||
node_modules
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Create .env file
|
||||
run: |
|
||||
echo "Creating .env file with secrets..."
|
||||
echo "MONGODB_URI=${{ secrets.MONGODB_URI }}" > .env
|
||||
echo "REDIS_URL=${{ secrets.REDIS_URL }}" >> .env
|
||||
echo "SESSION_SECRET=${{ secrets.SESSION_SECRET }}" >> .env
|
||||
echo "JWT_SECRET=${{ secrets.JWT_SECRET }}" >> .env
|
||||
echo "JWT_REFRESH_SECRET=${{ secrets.JWT_REFRESH_SECRET }}" >> .env
|
||||
echo "GOOGLE_CLIENT_ID=${{ secrets.GOOGLE_CLIENT_ID }}" >> .env
|
||||
echo "GOOGLE_CLIENT_SECRET=${{ secrets.GOOGLE_CLIENT_SECRET }}" >> .env
|
||||
echo "GOOGLE_REDIRECT_URI=${{ secrets.GOOGLE_REDIRECT_URI }}" >> .env
|
||||
echo "NEXT_PUBLIC_APP_URL=${{ secrets.NEXT_PUBLIC_APP_URL }}" >> .env
|
||||
echo "NODE_ENV=production" >> .env
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: false
|
||||
tags: siliconpin:latest
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
outputs: type=docker,dest=/tmp/siliconpin-image.tar
|
||||
|
||||
- name: Load Docker image
|
||||
run: docker load -i /tmp/siliconpin-image.tar
|
||||
|
||||
- name: Set up SSH
|
||||
uses: shimataro/ssh-key-action@v2
|
||||
with:
|
||||
key: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
known_hosts: 'just-a-placeholder'
|
||||
|
||||
- name: Add remote host to known hosts
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
echo "Adding ${{ secrets.SSH_HOST }} to known hosts..."
|
||||
ssh-keyscan -H ${{ secrets.SSH_HOST }} >> ~/.ssh/known_hosts
|
||||
chmod 600 ~/.ssh/known_hosts
|
||||
|
||||
- name: Compress Docker image
|
||||
run: |
|
||||
echo "Compressing Docker image..."
|
||||
gzip /tmp/siliconpin-image.tar
|
||||
mv /tmp/siliconpin-image.tar.gz siliconpin-image.tar.gz
|
||||
|
||||
- name: Transfer Docker image to server
|
||||
run: |
|
||||
echo "Transferring Docker image to server..."
|
||||
scp -o StrictHostKeyChecking=no siliconpin-image.tar.gz ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}:/root/services/siliconpin-image.tar.gz
|
||||
scp -o StrictHostKeyChecking=no docker-compose.yml ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}:/root/services/siliconpin/docker-compose.yml
|
||||
|
||||
- name: Deploy to production
|
||||
run: |
|
||||
ssh -o StrictHostKeyChecking=no ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} << 'EOF'
|
||||
# Force using bash instead of fish
|
||||
bash -c '
|
||||
# Navigate to the services directory
|
||||
cd /root/services/
|
||||
|
||||
# Create siliconpin directory if it doesn not exist
|
||||
mkdir -p siliconpin
|
||||
cd siliconpin
|
||||
|
||||
# Stop the current container
|
||||
docker compose down || true
|
||||
|
||||
# Load the new Docker image
|
||||
echo "Loading Docker image..."
|
||||
docker load < /root/services/siliconpin-image.tar.gz
|
||||
|
||||
# Remove the transferred image file
|
||||
rm -f /root/services/siliconpin-image.tar.gz
|
||||
|
||||
# Start the container using the new image
|
||||
echo "Starting container..."
|
||||
docker compose up -d
|
||||
|
||||
# Wait for container to be fully running
|
||||
echo "Waiting for container to be ready..."
|
||||
attempt=1
|
||||
max_attempts=30
|
||||
|
||||
until [ $attempt -gt $max_attempts ] || docker container inspect siliconpin --format="{{.State.Running}}" 2>/dev/null | grep -q "true"; do
|
||||
echo "Attempt $attempt/$max_attempts: Container not ready yet, waiting..."
|
||||
sleep 2
|
||||
attempt=$((attempt+1))
|
||||
done
|
||||
|
||||
if [ $attempt -gt $max_attempts ]; then
|
||||
echo "Container failed to start properly within time limit!"
|
||||
docker compose logs
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Further verification of application health
|
||||
echo "Verifying application health..."
|
||||
timeout=120
|
||||
start_time=$(date +%s)
|
||||
end_time=$((start_time + timeout))
|
||||
while [ $(date +%s) -lt $end_time ]; do
|
||||
if curl -s http://localhost:4023/ > /dev/null; then
|
||||
echo "Application is responding!"
|
||||
break
|
||||
fi
|
||||
echo "Waiting for application to respond..."
|
||||
sleep 10
|
||||
done
|
||||
|
||||
if ! curl -s http://localhost:4023/ > /dev/null; then
|
||||
echo "Application failed to respond within timeout!"
|
||||
docker compose logs
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Clean up is already done above
|
||||
|
||||
# Verify deployment
|
||||
echo "Deployment successful! Container is running and application is responding."
|
||||
docker ps | grep siliconpin
|
||||
'
|
||||
EOF
|
||||
57
.gitignore
vendored
Normal file
57
.gitignore
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
.pnp
|
||||
.pnp.js
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
|
||||
# Testing
|
||||
coverage/
|
||||
|
||||
# Next.js
|
||||
.next/
|
||||
out/
|
||||
|
||||
# Service Worker (auto-generated)
|
||||
public/sw.js
|
||||
public/workbox-*.js
|
||||
|
||||
# Production
|
||||
build/
|
||||
dist/
|
||||
Backups/
|
||||
# Misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# Debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Local env files
|
||||
.env.prod
|
||||
.env
|
||||
.env*
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# Vercel
|
||||
.vercel
|
||||
|
||||
# TypeScript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS
|
||||
Thumbs.db
|
||||
1
.husky/pre-commit
Executable file
1
.husky/pre-commit
Executable file
@@ -0,0 +1 @@
|
||||
yarn lint-staged
|
||||
462
CLAUDE.md
Normal file
462
CLAUDE.md
Normal file
@@ -0,0 +1,462 @@
|
||||
# CLAUDE.md - AI Assistant Context
|
||||
|
||||
## Project Overview
|
||||
|
||||
SiliconPin is a comprehensive web platform built with Next.js 15 that offers multiple services and tools for modern web development and hosting needs. The application provides:
|
||||
|
||||
- **Topic Management System**: Rich content creation and management with user ownership
|
||||
- **Admin Dashboard**: Comprehensive analytics, user management, and service administration
|
||||
- **Web Services**: Cloud hosting, VPN, Kubernetes deployment, and developer hiring
|
||||
- **PWA Capabilities**: Full Progressive Web App with offline functionality
|
||||
- **Speech Tools**: Text-to-speech and voice recognition utilities
|
||||
- **Payment System**: Balance management and billing integration
|
||||
- **Authentication**: JWT-based auth with Redis sessions and OAuth integration
|
||||
|
||||
## Technology Stack
|
||||
|
||||
### Core Framework
|
||||
|
||||
- **Next.js 15** - React framework with App Router
|
||||
- **TypeScript** - Type safety and better developer experience
|
||||
- **React 19** - Latest React with concurrent features
|
||||
|
||||
### UI & Styling
|
||||
|
||||
- **Tailwind CSS 4** - Utility-first CSS framework
|
||||
- **Custom UI Components** - Reusable React components with Tailwind styling
|
||||
- **next-themes** - Dark/light theme support
|
||||
- **Lucide React** - Beautiful, customizable icons
|
||||
- **BlockNote Editor** - Rich text editor with @blocknote/mantine (v0.25.2)
|
||||
|
||||
### Authentication & Sessions
|
||||
|
||||
- **JWT** - JSON Web Tokens for authentication
|
||||
- **bcryptjs** - Password hashing
|
||||
- **Redis** - Session storage (high performance)
|
||||
- **express-session** - Session management
|
||||
- **connect-redis** - Redis session store
|
||||
|
||||
### Database & Validation
|
||||
|
||||
- **MongoDB** - Document database with Mongoose ODM
|
||||
- **Mongoose** - MongoDB object modeling
|
||||
- **Zod** - Schema validation and type inference
|
||||
- **Topic System** - User-owned content with rich text editing and BlockNote editor
|
||||
|
||||
### State Management
|
||||
|
||||
- **TanStack Query v5** - Server state management
|
||||
- **React Context** - Client state management
|
||||
- **React Hook Form** - Form handling and validation
|
||||
|
||||
### Development Tools
|
||||
|
||||
- **ESLint** - Code linting with Next.js config
|
||||
- **Prettier** - Code formatting with Tailwind plugin
|
||||
- **Husky** - Git hooks for code quality
|
||||
- **lint-staged** - Run linters on staged files
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
siliconpin/
|
||||
├── app/ # Next.js App Router
|
||||
│ ├── api/ # Comprehensive API routes (auth, admin, topics, services, payments)
|
||||
│ ├── admin/ # Admin dashboard with analytics and management
|
||||
│ ├── auth/ # Authentication pages
|
||||
│ ├── topics/ # Topic management system (main content)
|
||||
│ ├── tools/ # Speech tools and utilities
|
||||
│ ├── services/ # Web services (hosting, VPN, K8s)
|
||||
│ ├── dashboard/ # User dashboard
|
||||
│ ├── globals.css # Global styles and Tailwind
|
||||
│ ├── layout.tsx # Root layout with providers
|
||||
│ └── page.tsx # Home page
|
||||
├── components/ # React components
|
||||
│ ├── admin/ # Admin-specific components
|
||||
│ ├── auth/ # Authentication forms and UI
|
||||
│ ├── topics/ # Topic/content components
|
||||
│ ├── tools/ # Tool-specific components
|
||||
│ ├── BlockNoteEditor/ # Rich text editor component
|
||||
│ ├── ui/ # Reusable UI library
|
||||
│ ├── header.tsx # Header with navigation
|
||||
│ ├── footer.tsx # Footer component
|
||||
│ └── theme-*.tsx # Theme components
|
||||
├── contexts/ # React contexts
|
||||
│ ├── AuthContext.tsx # Authentication state
|
||||
│ └── QueryProvider.tsx # TanStack Query provider
|
||||
├── lib/ # Utility libraries
|
||||
│ ├── auth-middleware.ts # API authentication middleware
|
||||
│ ├── jwt.ts # JWT utilities
|
||||
│ ├── mongodb.ts # Database connection
|
||||
│ ├── redis.ts # Redis connection
|
||||
│ ├── session.ts # Session configuration
|
||||
│ └── utils.ts # General utilities
|
||||
├── models/ # Database models
|
||||
│ ├── user.ts # User model with auth methods
|
||||
│ ├── topic.ts # Topic model with user ownership
|
||||
│ ├── billing.ts # Billing and payment models
|
||||
│ └── transaction.ts # Transaction tracking
|
||||
└── hooks/ # Custom React hooks
|
||||
```
|
||||
|
||||
## Key Features
|
||||
|
||||
### Authentication System
|
||||
|
||||
- JWT-based authentication with refresh tokens
|
||||
- Secure HTTP-only cookies
|
||||
- Redis session storage for high performance
|
||||
- Protected routes and API middleware
|
||||
- User registration, login, logout, token refresh
|
||||
- User ownership model for content (blogs, etc.)
|
||||
|
||||
### API Routes
|
||||
|
||||
- `POST /api/auth/register` - Create new user account
|
||||
- `POST /api/auth/login` - Sign in user
|
||||
- `POST /api/auth/logout` - Sign out user
|
||||
- `POST /api/auth/refresh` - Refresh access token
|
||||
- `GET /api/auth/me` - Get current user info (protected)
|
||||
|
||||
### Security Features
|
||||
|
||||
- Secure HTTP-only cookies
|
||||
- Password hashing with bcrypt
|
||||
- CSRF protection headers
|
||||
- Input validation with Zod
|
||||
- Protected API middleware
|
||||
- Session management with Redis
|
||||
|
||||
### Core Application Features
|
||||
|
||||
- **Topic Management** - User-owned content creation with rich text editing
|
||||
- **Admin Dashboard** - Comprehensive analytics, user management, and service administration
|
||||
- **Web Services** - Cloud hosting, VPN services, Kubernetes deployment, developer hiring
|
||||
- **Payment System** - Balance management, billing integration, and transaction tracking
|
||||
- **Speech Tools** - Text-to-speech and voice recognition utilities
|
||||
- **PWA Support** - Full Progressive Web App with offline capabilities and installability
|
||||
|
||||
## Development Commands
|
||||
|
||||
```bash
|
||||
# Development
|
||||
yarn dev # Start development server with hot reload
|
||||
yarn build # Build for production
|
||||
yarn start # Start production server
|
||||
|
||||
# Code quality
|
||||
yarn lint # Run ESLint with auto-fix
|
||||
yarn typecheck # Run TypeScript compiler (no output)
|
||||
yarn prepare # Set up Husky pre-commit hooks
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Required environment variables (create `.env.local`):
|
||||
|
||||
```env
|
||||
# Database
|
||||
MONGODB_URI=mongodb://localhost:27017/siliconpin
|
||||
|
||||
# Redis Session Store
|
||||
REDIS_URL=redis://localhost:6379
|
||||
|
||||
# Authentication Secrets
|
||||
SESSION_SECRET=your-session-secret-must-be-at-least-32-characters-long
|
||||
JWT_SECRET=your-jwt-secret-change-in-production
|
||||
JWT_REFRESH_SECRET=your-jwt-refresh-secret-change-in-production
|
||||
|
||||
# MinIO Storage (for topic images and file uploads)
|
||||
MINIO_ENDPOINT=your-minio-endpoint
|
||||
MINIO_PORT=9000
|
||||
MINIO_ACCESS_KEY=your-access-key
|
||||
MINIO_SECRET_KEY=your-secret-key
|
||||
MINIO_BUCKET=your-bucket-name
|
||||
|
||||
# Optional
|
||||
NODE_ENV=development
|
||||
PORT=3006
|
||||
```
|
||||
|
||||
## Common Development Tasks
|
||||
|
||||
### Adding New API Routes
|
||||
|
||||
1. Create new route file in `app/api/`
|
||||
2. Use the auth middleware for protected routes:
|
||||
|
||||
```typescript
|
||||
import { authMiddleware } from '@/lib/auth-middleware'
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const user = await authMiddleware(request)
|
||||
if (!user) {
|
||||
return Response.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
// Your protected logic here
|
||||
}
|
||||
```
|
||||
|
||||
### Creating New Components
|
||||
|
||||
1. Add component in `components/` directory
|
||||
2. Use custom UI components from components/ui/ when possible
|
||||
3. Follow existing patterns for styling and props
|
||||
4. Create index.ts barrel export for cleaner imports
|
||||
5. Use dynamic imports for heavy components (e.g., BlockNote)
|
||||
|
||||
### Adding New Database Models
|
||||
|
||||
1. Create model in `models/` directory
|
||||
2. Define Mongoose schema with proper types
|
||||
3. Add Zod validation schema
|
||||
4. Export both model and validation schema
|
||||
|
||||
### Form Handling Pattern
|
||||
|
||||
```typescript
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { z } from 'zod'
|
||||
|
||||
const schema = z.object({
|
||||
// Define your schema
|
||||
})
|
||||
|
||||
export function MyForm() {
|
||||
const form = useForm({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {
|
||||
// Set defaults
|
||||
},
|
||||
})
|
||||
|
||||
// Handle form submission
|
||||
}
|
||||
```
|
||||
|
||||
## AI Assistant Instructions
|
||||
|
||||
When working on this codebase:
|
||||
|
||||
### DO:
|
||||
|
||||
- Use TypeScript for all new code
|
||||
- Follow existing component patterns
|
||||
- Use custom UI components from components/ui/ when building UI
|
||||
- Implement proper error handling
|
||||
- Add Zod validation for API inputs
|
||||
- Follow the established folder structure
|
||||
- Use the existing authentication patterns
|
||||
- Implement responsive design with Tailwind
|
||||
- Add proper loading and error states
|
||||
|
||||
### DON'T:
|
||||
|
||||
- Mix different authentication patterns
|
||||
- Skip TypeScript types
|
||||
- Create components without proper props interface
|
||||
- Forget to handle loading/error states
|
||||
- Skip validation on API routes
|
||||
- Use different state management patterns
|
||||
- Add dependencies without checking existing ones
|
||||
|
||||
### Code Style
|
||||
|
||||
- Use functional components with hooks
|
||||
- Prefer composition over inheritance
|
||||
- Use descriptive variable and function names
|
||||
- Add JSDoc comments for complex functions
|
||||
- Follow Prettier formatting rules
|
||||
- Use consistent import ordering
|
||||
|
||||
### Key Pages
|
||||
|
||||
- `/auth` - Authentication and user management
|
||||
- `/dashboard` - User analytics and account overview
|
||||
- `/topics` - Public topic listing and content browsing
|
||||
- `/topics/new` - Create new topic (protected)
|
||||
- `/topics/[slug]` - View individual topic content
|
||||
- `/admin` - Admin dashboard with comprehensive management
|
||||
- `/services` - Web services (hosting, VPN, Kubernetes)
|
||||
- `/tools` - Speech tools and utilities
|
||||
|
||||
## 🔥 **CRITICAL: PWA Build vs Development Guidelines**
|
||||
|
||||
### **When You Need `yarn build` (Production Build Required):**
|
||||
|
||||
- ✅ **Service Worker Changes** - New caching strategies, SW updates
|
||||
- ✅ **Workbox Configuration** - Changes to `next.config.js` PWA settings
|
||||
- ✅ **Production PWA Testing** - Final validation before deployment
|
||||
- ✅ **Performance Optimization** - Testing minified/optimized PWA bundle
|
||||
|
||||
### **When `yarn dev` is Sufficient (No Build Needed):**
|
||||
|
||||
- ✅ **Manifest Changes** - Icons, name, description, screenshots, theme colors
|
||||
- ✅ **PWA Validation** - Chrome DevTools Application tab checks
|
||||
- ✅ **Install Button Testing** - Browser install prompts
|
||||
- ✅ **Icon Updates** - New icon files or sizes
|
||||
- ✅ **Most PWA Features** - Installability, offline detection, etc.
|
||||
|
||||
### **Quick PWA Development Workflow:**
|
||||
|
||||
```bash
|
||||
# 1. Start development (most PWA features work)
|
||||
yarn dev
|
||||
|
||||
# 2. Make manifest/icon changes
|
||||
# 3. Refresh browser - changes appear immediately
|
||||
# 4. Validate in Chrome DevTools → Application → Manifest
|
||||
|
||||
# 5. Only build when deploying or testing service worker
|
||||
yarn build && yarn start
|
||||
```
|
||||
|
||||
**🚨 KEY INSIGHT**: PWA manifest changes hot-reload in development mode, making `yarn build` unnecessary for most PWA validation and testing!
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **BlockNote Editor SideMenu Error**
|
||||
- **Solution**: Ensure using `@blocknote/mantine` v0.25.2 (not newer versions)
|
||||
- Import from `@blocknote/mantine` not `@blocknote/react` for BlockNoteView
|
||||
- Create index.ts barrel export in BlockNoteEditor folder
|
||||
- Clear cache: `rm -rf .next node_modules/.cache`
|
||||
- Clean install: `rm -rf node_modules yarn.lock && yarn install`
|
||||
|
||||
2. **MongoDB Connection Issues**
|
||||
- Ensure MongoDB is running locally or connection string is correct
|
||||
- Check network connectivity for cloud databases
|
||||
|
||||
3. **Redis Connection Issues**
|
||||
- Verify Redis server is running locally
|
||||
- Check REDIS_URL format: `redis://localhost:6379`
|
||||
|
||||
4. **Authentication Not Working**
|
||||
- Check JWT secrets are set in environment variables
|
||||
- Verify cookies are being set correctly
|
||||
- Check browser developer tools for errors
|
||||
|
||||
5. **Build Errors**
|
||||
- Run `yarn typecheck` to identify TypeScript errors
|
||||
- Check for unused imports or variables
|
||||
- Verify all environment variables are set
|
||||
|
||||
6. **Development Server Issues**
|
||||
- Clear Next.js cache: `rm -rf .next`
|
||||
- Reinstall dependencies: `rm -rf node_modules && yarn install`
|
||||
|
||||
## Deployment Notes
|
||||
|
||||
- This application is optimized for Vercel deployment
|
||||
- Environment variables must be set in production
|
||||
- Ensure MongoDB and Redis instances are accessible from production
|
||||
- Use secure secrets for JWT and session keys
|
||||
- Enable HTTPS in production for secure cookies
|
||||
|
||||
## Contributing
|
||||
|
||||
When extending this application:
|
||||
|
||||
1. Maintain the established patterns
|
||||
2. Add proper TypeScript types
|
||||
3. Include validation schemas
|
||||
4. Update documentation as needed
|
||||
5. Test authentication flows
|
||||
6. Follow security best practices
|
||||
|
||||
---
|
||||
|
||||
## Application Status
|
||||
|
||||
### ✅ **PRODUCTION READY - COMPREHENSIVE PLATFORM**
|
||||
|
||||
SiliconPin has evolved into a complete web platform with the following implemented features:
|
||||
|
||||
#### **🔐 Authentication & User Management**
|
||||
|
||||
- JWT-based authentication with refresh tokens
|
||||
- OAuth integration (Google, GitHub)
|
||||
- Redis session management
|
||||
- Protected routes and API middleware
|
||||
- User profile management
|
||||
|
||||
#### **📝 Topic Management System**
|
||||
|
||||
- Rich text editing with BlockNote editor
|
||||
- User-owned content creation and management
|
||||
- Tag system with search and filtering
|
||||
- Image upload with MinIO integration
|
||||
- Draft/publish workflows
|
||||
- SEO optimization with metadata
|
||||
|
||||
#### **🛠️ Admin Dashboard**
|
||||
|
||||
- Comprehensive analytics and reporting
|
||||
- User management and moderation
|
||||
- Billing and transaction tracking
|
||||
- Service administration
|
||||
- System settings and configuration
|
||||
- PDF report generation
|
||||
|
||||
#### **🌐 Web Services**
|
||||
|
||||
- Cloud hosting deployment and management
|
||||
- VPN service configuration
|
||||
- Kubernetes orchestration
|
||||
- Developer hiring marketplace
|
||||
- Service monitoring and health checks
|
||||
|
||||
#### **🎤 Speech Tools**
|
||||
|
||||
- Text-to-speech synthesis with multiple voices
|
||||
- Voice recognition and transcription
|
||||
- Web Speech API integration
|
||||
- Audio processing utilities
|
||||
|
||||
#### **💰 Payment & Billing System**
|
||||
|
||||
- Balance management and top-up
|
||||
- Transaction history and tracking
|
||||
- Service billing and invoicing
|
||||
- Payment gateway integration
|
||||
- Refund processing
|
||||
|
||||
#### **📱 PWA Implementation**
|
||||
|
||||
- Full Progressive Web App capabilities
|
||||
- Offline functionality with service worker
|
||||
- App installation and native feel
|
||||
- Responsive design across devices
|
||||
- Push notification support
|
||||
|
||||
### **🚀 Technical Architecture**
|
||||
|
||||
#### **Backend Infrastructure**
|
||||
|
||||
- **50+ API Endpoints**: Comprehensive REST API coverage
|
||||
- **8 Database Models**: User, Topic, Billing, Transaction, etc.
|
||||
- **MongoDB Integration**: Full CRUD operations with Mongoose
|
||||
- **Redis Caching**: High-performance session and data caching
|
||||
- **MinIO Storage**: File and image management
|
||||
|
||||
#### **Frontend Excellence**
|
||||
|
||||
- **150+ Component Library**: Reusable React components
|
||||
- **TypeScript**: 100% type safety with strict mode
|
||||
- **Tailwind CSS**: Modern, responsive design system
|
||||
- **Next.js 15**: Latest framework with App Router
|
||||
- **PWA Ready**: Installable with offline capabilities
|
||||
|
||||
#### **Development Quality**
|
||||
|
||||
- **Code Quality**: ESLint, Prettier, and Husky pre-commit hooks
|
||||
- **Security**: Input validation, CSRF protection, secure cookies
|
||||
- **Performance**: Optimized bundle, lazy loading, caching strategies
|
||||
- **Scalability**: Microservice-ready architecture
|
||||
- **Documentation**: Comprehensive developer documentation
|
||||
|
||||
**Status**: **PRODUCTION DEPLOYMENT READY** with full feature set and enterprise-grade architecture.
|
||||
57
Dockerfile
Normal file
57
Dockerfile
Normal file
@@ -0,0 +1,57 @@
|
||||
# ---------- Stage 1: Dependencies ----------
|
||||
FROM node:20-alpine AS deps
|
||||
WORKDIR /app
|
||||
|
||||
# Install dependencies needed to compile some Node modules
|
||||
RUN apk add --no-cache libc6-compat
|
||||
|
||||
# Copy dependency declarations
|
||||
COPY package.json yarn.lock ./
|
||||
|
||||
# Install production and build dependencies
|
||||
RUN yarn install --frozen-lockfile --network-timeout 402300
|
||||
|
||||
# ---------- Stage 2: Build ----------
|
||||
FROM node:20-alpine AS builder
|
||||
WORKDIR /app
|
||||
|
||||
ENV NEXT_TELEMETRY_DISABLED=1 \
|
||||
SKIP_ENV_VALIDATION=true \
|
||||
NODE_ENV=production
|
||||
|
||||
# Copy installed deps
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY --from=deps /app/package.json ./
|
||||
COPY --from=deps /app/yarn.lock ./
|
||||
|
||||
# Copy all source files
|
||||
COPY . .
|
||||
|
||||
# Build the application
|
||||
RUN yarn build
|
||||
|
||||
# ---------- Stage 3: Runner ----------
|
||||
FROM node:20-alpine AS runner
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV=production \
|
||||
NEXT_TELEMETRY_DISABLED=1 \
|
||||
PORT=4023 \
|
||||
HOSTNAME=0.0.0.0
|
||||
|
||||
# Create a non-root user
|
||||
RUN addgroup -g 1001 -S nodejs && \
|
||||
adduser -u 1001 -S nextjs -G nodejs
|
||||
|
||||
# Copy the minimal standalone app
|
||||
COPY --from=builder /app/.next/standalone ./
|
||||
COPY --from=builder /app/.next/static ./.next/static
|
||||
COPY --from=builder /app/public ./public
|
||||
|
||||
# Ensure correct ownership and drop root
|
||||
RUN chown -R nextjs:nodejs /app
|
||||
USER nextjs
|
||||
|
||||
EXPOSE 4023
|
||||
|
||||
CMD ["node", "server.js"]
|
||||
46
Makefile
Normal file
46
Makefile
Normal file
@@ -0,0 +1,46 @@
|
||||
# Makefile for SiliconPin
|
||||
|
||||
COMPOSE_PROD=docker-compose.prod.yml
|
||||
COMPOSE_DEV=docker-compose.dev.yml
|
||||
|
||||
# Production commands
|
||||
up:
|
||||
docker-compose -f $(COMPOSE_PROD) up -d
|
||||
|
||||
build:
|
||||
docker-compose -f $(COMPOSE_PROD) up -d --build
|
||||
|
||||
down:
|
||||
docker-compose -f $(COMPOSE_PROD) down
|
||||
|
||||
down-volumes:
|
||||
docker-compose -f $(COMPOSE_PROD) down -v
|
||||
|
||||
logs:
|
||||
docker-compose -f $(COMPOSE_PROD) logs -f
|
||||
|
||||
ps:
|
||||
docker-compose -f $(COMPOSE_PROD) ps
|
||||
|
||||
restart:
|
||||
docker-compose -f $(COMPOSE_PROD) restart
|
||||
|
||||
health:
|
||||
docker inspect --format='{{.Name}}: {{range .State.Health.Log}}{{.ExitCode}} {{.Output}}{{end}}' $$(docker ps -q)
|
||||
|
||||
# Development commands
|
||||
dev:
|
||||
docker-compose -f $(COMPOSE_DEV) up -d
|
||||
|
||||
dev-down:
|
||||
docker-compose -f $(COMPOSE_DEV) down
|
||||
|
||||
dev-logs:
|
||||
docker-compose -f $(COMPOSE_DEV) logs -f
|
||||
|
||||
#Backup commands
|
||||
backup-mongo-dev:
|
||||
docker exec siliconpin-mongo-dev mongodump --archive=/data/db/sp_mongo_dev.archive && mkdir -p Backups && docker cp siliconpin-mongo-dev:/data/db/sp_mongo_dev.archive ./Backups/sp_mongo_dev.archive
|
||||
|
||||
backup-mongo-prod:
|
||||
docker exec mongo_sp mongodump --archive=/data/db/sp_mongo_prod.archive && mkdir -p Backups && docker cp mongo_sp:/data/db/sp_mongo_prod.archive ./Backups/sp_mongo_prod.archive
|
||||
348
README.md
Normal file
348
README.md
Normal file
@@ -0,0 +1,348 @@
|
||||
# SiliconPin
|
||||
|
||||
A comprehensive web platform offering topic management, admin dashboards, web services, payment processing, and Progressive Web App capabilities.
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
```bash
|
||||
# Start services and development
|
||||
make dev && yarn && yarn dev
|
||||
```
|
||||
|
||||
## ✨ Features
|
||||
|
||||
### 🔐 **Authentication & User Management**
|
||||
|
||||
- JWT-based authentication with refresh tokens
|
||||
- OAuth integration (Google, GitHub)
|
||||
- Redis session storage for high performance
|
||||
- Protected routes and comprehensive API middleware
|
||||
- User profile management and account settings
|
||||
|
||||
### 📝 **Topic Management System**
|
||||
|
||||
- Rich text editing with BlockNote editor
|
||||
- User-owned content creation and management
|
||||
- Dynamic tag system with search and filtering
|
||||
- Image upload with MinIO integration
|
||||
- Draft/publish workflows with version control
|
||||
- SEO optimization with metadata and structured data
|
||||
|
||||
### 🛠️ **Admin Dashboard**
|
||||
|
||||
- Comprehensive analytics and reporting
|
||||
- User management and moderation tools
|
||||
- Billing and transaction tracking
|
||||
- Service administration and monitoring
|
||||
- System settings and configuration
|
||||
- PDF report generation
|
||||
|
||||
### 🌐 **Web Services**
|
||||
|
||||
- Cloud hosting deployment and management
|
||||
- VPN service configuration and setup
|
||||
- Kubernetes orchestration and container management
|
||||
- Developer hiring marketplace
|
||||
- Service monitoring and health checks
|
||||
|
||||
### 🎤 **Speech Tools**
|
||||
|
||||
- Text-to-speech synthesis with multiple voices
|
||||
- Voice recognition and transcription
|
||||
- Web Speech API integration
|
||||
- Audio processing utilities
|
||||
|
||||
### 💰 **Payment & Billing System**
|
||||
|
||||
- Balance management and top-up functionality
|
||||
- Transaction history and detailed tracking
|
||||
- Service billing and automated invoicing
|
||||
- Payment gateway integration
|
||||
- Refund processing and management
|
||||
|
||||
### 📱 **Progressive Web App (PWA)**
|
||||
|
||||
- Full PWA capabilities with offline functionality
|
||||
- App installation and native-like experience
|
||||
- Service worker for caching and background sync
|
||||
- Responsive design across all devices
|
||||
- Push notification support
|
||||
|
||||
### 🎨 **Modern UI/UX**
|
||||
|
||||
- Next.js 15 with App Router architecture
|
||||
- TypeScript for complete type safety
|
||||
- Tailwind CSS with custom design system
|
||||
- Radix UI components for accessibility
|
||||
- Dark/light theme support
|
||||
- Mobile-first responsive design
|
||||
|
||||
## 🛠️ Installation & Setup
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js 18+
|
||||
- MongoDB (local or cloud)
|
||||
- Redis (local or cloud)
|
||||
- Yarn package manager
|
||||
|
||||
### Development Setup
|
||||
|
||||
1. **Clone and install:**
|
||||
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd siliconpin
|
||||
yarn install
|
||||
```
|
||||
|
||||
2. **Environment configuration:**
|
||||
|
||||
```bash
|
||||
cp .env.example .env.local
|
||||
```
|
||||
|
||||
Configure `.env.local`:
|
||||
|
||||
```env
|
||||
# Database
|
||||
MONGODB_URI=mongodb://localhost:27017/siliconpin
|
||||
|
||||
# Redis Session Store
|
||||
REDIS_URL=redis://localhost:6379
|
||||
|
||||
# Authentication
|
||||
SESSION_SECRET=your-secure-32-character-session-secret
|
||||
JWT_SECRET=your-jwt-secret-change-in-production
|
||||
JWT_REFRESH_SECRET=your-jwt-refresh-secret-change-in-production
|
||||
|
||||
# MinIO Storage (for file uploads)
|
||||
MINIO_ENDPOINT=your-minio-endpoint
|
||||
MINIO_PORT=9000
|
||||
MINIO_ACCESS_KEY=your-access-key
|
||||
MINIO_SECRET_KEY=your-secret-key
|
||||
MINIO_BUCKET=your-bucket-name
|
||||
```
|
||||
|
||||
3. **Start development:**
|
||||
|
||||
```bash
|
||||
# Start MongoDB and Redis
|
||||
make dev
|
||||
|
||||
# Start Next.js development server
|
||||
yarn dev
|
||||
```
|
||||
|
||||
4. **Access the application:**
|
||||
|
||||
- Main site: [http://localhost:4023](http://localhost:4023)
|
||||
- Authentication: [http://localhost:4023/auth](http://localhost:4023/auth)
|
||||
- Admin dashboard: [http://localhost:4023/admin](http://localhost:4023/admin)
|
||||
- Topics: [http://localhost:4023/topics](http://localhost:4023/topics)
|
||||
- Tools: [http://localhost:4023/tools](http://localhost:4023/tools)
|
||||
|
||||
## 🏗️ Architecture
|
||||
|
||||
### Project Structure
|
||||
|
||||
```
|
||||
siliconpin/
|
||||
├── app/ # Next.js App Router
|
||||
│ ├── api/ # Comprehensive API (50+ endpoints)
|
||||
│ │ ├── auth/ # Authentication endpoints
|
||||
│ │ ├── admin/ # Admin management
|
||||
│ │ ├── topics/ # Content management
|
||||
│ │ ├── services/ # Service deployment
|
||||
│ │ ├── payments/ # Payment processing
|
||||
│ │ └── tools/ # Utility APIs
|
||||
│ ├── admin/ # Admin dashboard
|
||||
│ ├── auth/ # Authentication pages
|
||||
│ ├── topics/ # Topic management
|
||||
│ ├── services/ # Web services
|
||||
│ ├── tools/ # Speech tools
|
||||
│ ├── dashboard/ # User dashboard
|
||||
│ └── layout.tsx # Root layout
|
||||
├── components/ # React components (150+)
|
||||
│ ├── admin/ # Admin-specific UI
|
||||
│ ├── auth/ # Authentication forms
|
||||
│ ├── topics/ # Content components
|
||||
│ ├── tools/ # Tool interfaces
|
||||
│ ├── ui/ # Reusable UI library
|
||||
│ └── BlockNoteEditor/ # Rich text editor
|
||||
├── lib/ # Utilities and services
|
||||
│ ├── mongodb.ts # Database connection
|
||||
│ ├── redis.ts # Redis caching
|
||||
│ ├── minio.ts # File storage
|
||||
│ ├── auth-middleware.ts # API security
|
||||
│ └── billing-service.ts # Payment logic
|
||||
├── models/ # Database models (8 schemas)
|
||||
│ ├── user.ts # User accounts
|
||||
│ ├── topic.ts # Content model
|
||||
│ ├── billing.ts # Payment data
|
||||
│ └── transaction.ts # Transaction records
|
||||
├── contexts/ # React contexts
|
||||
├── hooks/ # Custom React hooks
|
||||
└── docs/ # Documentation
|
||||
```
|
||||
|
||||
## 🔗 API Overview
|
||||
|
||||
### Core APIs (50+ endpoints)
|
||||
|
||||
**Authentication & Users**
|
||||
|
||||
- `POST /api/auth/register` - User registration
|
||||
- `POST /api/auth/login` - User authentication
|
||||
- `POST /api/auth/refresh` - Token refresh
|
||||
- `GET /api/auth/me` - Current user profile
|
||||
- `POST /api/auth/google` - OAuth authentication
|
||||
|
||||
**Topic Management**
|
||||
|
||||
- `GET /api/topics` - List topics with search/filter
|
||||
- `POST /api/topics` - Create new topic
|
||||
- `GET /api/topics/[slug]` - Get specific topic
|
||||
- `PUT /api/topic/[id]` - Update topic
|
||||
- `DELETE /api/topic/[id]` - Delete topic
|
||||
|
||||
**Admin Dashboard**
|
||||
|
||||
- `GET /api/admin/dashboard` - Analytics data
|
||||
- `GET /api/admin/users` - User management
|
||||
- `GET /api/admin/billing` - Billing overview
|
||||
- `GET /api/admin/reports` - Generate reports
|
||||
|
||||
**Services & Deployment**
|
||||
|
||||
- `POST /api/services/deploy-kubernetes` - K8s deployment
|
||||
- `POST /api/services/deploy-vpn` - VPN setup
|
||||
- `POST /api/services/deploy-cloude` - Cloud instances
|
||||
- `POST /api/services/hire-developer` - Developer requests
|
||||
|
||||
**Payment & Billing**
|
||||
|
||||
- `POST /api/payments/initiate` - Start payment
|
||||
- `GET /api/user/balance` - User balance
|
||||
- `POST /api/balance/add` - Add funds
|
||||
- `GET /api/transactions` - Transaction history
|
||||
|
||||
**File Management**
|
||||
|
||||
- `POST /api/upload` - File upload to MinIO
|
||||
- `POST /api/topic-content-image` - Topic images
|
||||
|
||||
## 📦 Development Scripts
|
||||
|
||||
```bash
|
||||
# Development
|
||||
yarn dev # Start development server (port 4023)
|
||||
yarn build # Build for production
|
||||
yarn start # Start production server
|
||||
|
||||
# Code Quality
|
||||
yarn lint # Run ESLint with auto-fix
|
||||
yarn typecheck # TypeScript compilation check
|
||||
yarn format # Format code with Prettier
|
||||
|
||||
# Database Management
|
||||
yarn db:seed # Seed database with initial data
|
||||
yarn db:reset # Reset database to clean state
|
||||
|
||||
# Docker
|
||||
make dev # Start MongoDB & Redis containers
|
||||
docker-compose up # Full containerized deployment
|
||||
```
|
||||
|
||||
## 🔒 Security & Architecture
|
||||
|
||||
### Security Features
|
||||
|
||||
- JWT authentication with HTTP-only cookies
|
||||
- OAuth integration (Google, GitHub)
|
||||
- Password hashing with bcryptjs
|
||||
- CSRF protection headers
|
||||
- Input validation with Zod schemas
|
||||
- Protected API routes with middleware
|
||||
- Redis session management
|
||||
- Content Security Policy headers
|
||||
- Rate limiting ready for production
|
||||
|
||||
### Technical Stack
|
||||
|
||||
- **Frontend**: Next.js 15, React 19, TypeScript
|
||||
- **Styling**: Tailwind CSS, Radix UI, Custom components
|
||||
- **Backend**: Node.js, MongoDB, Redis, MinIO
|
||||
- **Authentication**: JWT, OAuth, Sessions
|
||||
- **State**: TanStack Query, React Context
|
||||
- **Validation**: Zod schemas, React Hook Form
|
||||
- **PWA**: Service Worker, Workbox, Manifest
|
||||
|
||||
## 🚀 Deployment & Production
|
||||
|
||||
### Supported Platforms
|
||||
|
||||
- **Vercel** (Recommended) - Zero-config Next.js deployment
|
||||
- **Railway** - Full-stack with managed databases
|
||||
- **DigitalOcean App Platform** - Container deployment
|
||||
- **Docker** - Self-hosted containerized deployment
|
||||
- **AWS ECS/Lambda** - Enterprise cloud deployment
|
||||
|
||||
### Production Checklist
|
||||
|
||||
- [ ] Environment variables configured
|
||||
- [ ] MongoDB Atlas or production database
|
||||
- [ ] Redis Cloud or managed instance
|
||||
- [ ] MinIO or S3 for file storage
|
||||
- [ ] SSL/HTTPS certificates
|
||||
- [ ] Domain and DNS configuration
|
||||
- [ ] Security headers and CORS
|
||||
- [ ] Rate limiting implementation
|
||||
- [ ] Error tracking (Sentry)
|
||||
- [ ] Analytics and monitoring
|
||||
|
||||
### Docker Deployment
|
||||
|
||||
```bash
|
||||
# Production deployment
|
||||
docker-compose --profile production up -d
|
||||
|
||||
# With custom environment
|
||||
docker-compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
## 📚 Documentation
|
||||
|
||||
Comprehensive documentation is available in the `docs/` directory:
|
||||
|
||||
- **[API Documentation](docs/API.md)** - Complete API reference
|
||||
- **[Deployment Guide](docs/DEPLOYMENT.md)** - Platform-specific deployment
|
||||
- **[Code Patterns](docs/PATTERNS.md)** - Development patterns and examples
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
### Development Guidelines
|
||||
|
||||
1. Follow TypeScript strict mode
|
||||
2. Use Zod for validation
|
||||
3. Implement proper error handling
|
||||
4. Add tests for new features
|
||||
5. Update documentation
|
||||
6. Follow existing code patterns
|
||||
|
||||
### Code Quality
|
||||
|
||||
Pre-commit hooks ensure:
|
||||
|
||||
- ESLint compliance
|
||||
- Prettier formatting
|
||||
- TypeScript compilation
|
||||
- Test passing (when applicable)
|
||||
|
||||
## 📄 License
|
||||
|
||||
MIT License - Open source and free for commercial use.
|
||||
|
||||
---
|
||||
|
||||
**SiliconPin** - A comprehensive web platform combining topic management, admin dashboards, web services, payment processing, and Progressive Web App capabilities in a modern, scalable architecture.
|
||||
BIN
Screenshot.png
Normal file
BIN
Screenshot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 345 KiB |
190
app/about/page.tsx
Normal file
190
app/about/page.tsx
Normal file
@@ -0,0 +1,190 @@
|
||||
import { Metadata } from 'next'
|
||||
import { generateMetadata } from '@/lib/seo'
|
||||
import { Header } from '@/components/header'
|
||||
import { Footer } from '@/components/footer'
|
||||
import { CustomSolutionCTA } from '@/components/ui/custom-solution-cta'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Shield, Zap, Users, Globe, Server, Cpu } from 'lucide-react'
|
||||
|
||||
export const metadata: Metadata = generateMetadata({
|
||||
title: 'About Us - SiliconPin',
|
||||
description:
|
||||
'Learn more about SiliconPin, our mission, values, and the team behind our high-performance hosting solutions and developer services.',
|
||||
})
|
||||
|
||||
export default function AboutPage() {
|
||||
const coreValues = [
|
||||
{
|
||||
title: 'Reliability',
|
||||
description: 'We understand that downtime means lost business. That\'s why we prioritize reliability in everything we do, from our infrastructure to our customer service.',
|
||||
icon: Server,
|
||||
color: 'text-blue-600'
|
||||
},
|
||||
{
|
||||
title: 'Security',
|
||||
description: 'In an increasingly digital world, security is paramount. We implement robust security measures to protect our clients\' data and applications.',
|
||||
icon: Shield,
|
||||
color: 'text-green-600'
|
||||
},
|
||||
{
|
||||
title: 'Innovation',
|
||||
description: 'We continuously explore new technologies and methodologies to improve our services and provide our clients with cutting-edge solutions.',
|
||||
icon: Zap,
|
||||
color: 'text-purple-600'
|
||||
},
|
||||
{
|
||||
title: 'Customer Focus',
|
||||
description: 'Our clients\' success is our success. We work closely with our clients to understand their needs and provide tailored solutions that help them achieve their goals.',
|
||||
icon: Users,
|
||||
color: 'text-orange-600'
|
||||
}
|
||||
]
|
||||
|
||||
const whyChooseUs = [
|
||||
'24/7 expert technical support',
|
||||
'99.9% uptime guarantee',
|
||||
'Scalable infrastructure to grow with your business',
|
||||
'Comprehensive security measures including DDoS protection and SSL certificates',
|
||||
'Expertise across various technologies including PHP, Node.js, Python, and Kubernetes',
|
||||
'Commitment to customer success through personalized service'
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<Header />
|
||||
<main className="container max-w-6xl pt-24 pb-8">
|
||||
{/* Page Header */}
|
||||
<div className="text-center mb-12">
|
||||
<h1 className="text-4xl font-bold tracking-tight mb-4">About SiliconPin</h1>
|
||||
<p className="text-xl text-muted-foreground max-w-3xl mx-auto">
|
||||
We promote awareness about digital freedom by providing software and hardware development and research
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-8">
|
||||
{/* Our Story */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl">Our Story</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<p className="text-muted-foreground leading-relaxed">
|
||||
SiliconPin was founded in 2021 with a clear mission: to provide businesses of all sizes with reliable,
|
||||
high-performance hosting solutions that enable growth and innovation. What started as a small team of
|
||||
passionate developers and system administrators has grown into a trusted hosting provider serving clients
|
||||
across various industries.
|
||||
</p>
|
||||
<p className="text-muted-foreground leading-relaxed">
|
||||
Based in Habra, West Bengal, India, our team combines technical expertise with a deep understanding of
|
||||
business needs to deliver hosting solutions that are not just technically sound but also aligned with
|
||||
our clients' business objectives.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Our Mission */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl">Our Mission</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-muted-foreground leading-relaxed">
|
||||
At SiliconPin, our mission is to empower businesses through technology by providing reliable, secure,
|
||||
and scalable hosting solutions. We believe that technology should enable businesses to focus on what
|
||||
they do best, without worrying about infrastructure management or technical complexities.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Our Values */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl">Our Values</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{coreValues.map((value, index) => {
|
||||
const Icon = value.icon
|
||||
return (
|
||||
<div key={index} className="space-y-3">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className={`p-2 rounded-lg bg-primary/10`}>
|
||||
<Icon className={`w-5 h-5 ${value.color}`} />
|
||||
</div>
|
||||
<h3 className="text-lg font-medium">{value.title}</h3>
|
||||
</div>
|
||||
<p className="text-muted-foreground leading-relaxed">
|
||||
{value.description}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Why Choose Us */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl">Why Choose SiliconPin?</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{whyChooseUs.map((reason, index) => (
|
||||
<div key={index} className="flex items-start space-x-3">
|
||||
<div className="w-2 h-2 bg-green-600 rounded-full mt-2 shrink-0"></div>
|
||||
<span className="text-muted-foreground">{reason}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Location & Contact */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl flex items-center">
|
||||
<Globe className="w-6 h-6 mr-2 text-blue-600" />
|
||||
Our Location
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
<div>
|
||||
<h3 className="font-medium mb-2">Headquarters</h3>
|
||||
<p className="text-muted-foreground">
|
||||
121 Lalbari, GourBongo Road<br />
|
||||
Habra, West Bengal 743271<br />
|
||||
India
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium mb-2">Get in Touch</h3>
|
||||
<div className="space-y-2 text-muted-foreground">
|
||||
<p>Phone: +91-700-160-1485</p>
|
||||
<p>Email: support@siliconpin.com</p>
|
||||
<div className="flex gap-2 mt-4">
|
||||
<Badge variant="outline">Founded 2021</Badge>
|
||||
<Badge variant="outline">India Based</Badge>
|
||||
<Badge variant="outline">24/7 Support</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* CTA Section */}
|
||||
<CustomSolutionCTA
|
||||
title="Ready to Get Started?"
|
||||
description="Join hundreds of businesses who trust SiliconPin for their hosting and development needs"
|
||||
buttonText="Get in Touch"
|
||||
buttonHref="/contact"
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
28
app/admin/analytics/page.tsx
Normal file
28
app/admin/analytics/page.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
'use client'
|
||||
|
||||
import AdminLayout from '@/components/admin/AdminLayout'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
|
||||
export default function AdminAnalyticsPage() {
|
||||
return (
|
||||
<AdminLayout>
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">Analytics</h1>
|
||||
<p className="text-gray-600 mt-2">Advanced analytics and insights</p>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Analytics Dashboard</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<p className="text-gray-500">Analytics dashboard coming soon...</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</AdminLayout>
|
||||
)
|
||||
}
|
||||
19
app/admin/billing/page.tsx
Normal file
19
app/admin/billing/page.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
'use client'
|
||||
|
||||
import AdminLayout from '@/components/admin/AdminLayout'
|
||||
import BillingManagement from '@/components/admin/BillingManagement'
|
||||
|
||||
export default function AdminBillingPage() {
|
||||
return (
|
||||
<AdminLayout>
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">Billing Management</h1>
|
||||
<p className="text-gray-600 mt-2">Manage billing records, payments, and refunds</p>
|
||||
</div>
|
||||
|
||||
<BillingManagement />
|
||||
</div>
|
||||
</AdminLayout>
|
||||
)
|
||||
}
|
||||
256
app/admin/page.tsx
Normal file
256
app/admin/page.tsx
Normal file
@@ -0,0 +1,256 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import AdminLayout from '@/components/admin/AdminLayout'
|
||||
import DashboardStats from '@/components/admin/DashboardStats'
|
||||
import RecentActivity from '@/components/admin/RecentActivity'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { toast } from '@/hooks/use-toast'
|
||||
|
||||
interface DashboardData {
|
||||
users: {
|
||||
total: number
|
||||
active: number
|
||||
newThisMonth: number
|
||||
verified: number
|
||||
verificationRate: string
|
||||
}
|
||||
revenue: {
|
||||
total: number
|
||||
monthly: number
|
||||
weekly: number
|
||||
}
|
||||
transactions: {
|
||||
total: number
|
||||
monthly: number
|
||||
}
|
||||
services: {
|
||||
total: number
|
||||
active: number
|
||||
breakdown: Array<{
|
||||
_id: string
|
||||
count: number
|
||||
revenue: number
|
||||
}>
|
||||
}
|
||||
developerRequests: {
|
||||
total: number
|
||||
pending: number
|
||||
}
|
||||
recentActivity: {
|
||||
users: Array<{
|
||||
_id: string
|
||||
name: string
|
||||
email: string
|
||||
siliconId: string
|
||||
createdAt: string
|
||||
}>
|
||||
transactions: Array<{
|
||||
_id: string
|
||||
type: string
|
||||
amount: number
|
||||
description: string
|
||||
status: string
|
||||
createdAt: string
|
||||
userId: {
|
||||
name: string
|
||||
email: string
|
||||
siliconId: string
|
||||
}
|
||||
}>
|
||||
}
|
||||
}
|
||||
|
||||
export default function AdminDashboard() {
|
||||
const [data, setData] = useState<DashboardData | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
fetchDashboardData()
|
||||
}, [])
|
||||
|
||||
const fetchDashboardData = async () => {
|
||||
try {
|
||||
const token =
|
||||
localStorage.getItem('token') ||
|
||||
document.cookie
|
||||
.split('; ')
|
||||
.find((row) => row.startsWith('token='))
|
||||
?.split('=')[1]
|
||||
|
||||
const response = await fetch('/api/admin/dashboard', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch dashboard data')
|
||||
}
|
||||
|
||||
const dashboardData = await response.json()
|
||||
setData(dashboardData)
|
||||
} catch (error) {
|
||||
console.error('Dashboard fetch error:', error)
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'Failed to load dashboard data',
|
||||
variant: 'destructive',
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<AdminLayout>
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">Dashboard</h1>
|
||||
<p className="text-gray-600 mt-2">Overview of your SiliconPin platform</p>
|
||||
</div>
|
||||
|
||||
{/* Loading skeletons */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
<Card key={i}>
|
||||
<CardHeader>
|
||||
<Skeleton className="h-4 w-24" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Skeleton className="h-8 w-16 mb-2" />
|
||||
<Skeleton className="h-3 w-32" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Skeleton className="h-6 w-32" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{Array.from({ length: 3 }).map((_, i) => (
|
||||
<div key={i} className="flex justify-between">
|
||||
<div>
|
||||
<Skeleton className="h-4 w-24 mb-1" />
|
||||
<Skeleton className="h-3 w-32" />
|
||||
</div>
|
||||
<Skeleton className="h-3 w-16" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Skeleton className="h-6 w-32" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{Array.from({ length: 3 }).map((_, i) => (
|
||||
<div key={i} className="flex justify-between">
|
||||
<div>
|
||||
<Skeleton className="h-4 w-24 mb-1" />
|
||||
<Skeleton className="h-3 w-32" />
|
||||
</div>
|
||||
<Skeleton className="h-3 w-16" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</AdminLayout>
|
||||
)
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
<AdminLayout>
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<p className="text-gray-500">Failed to load dashboard data</p>
|
||||
</div>
|
||||
</AdminLayout>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<AdminLayout>
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-10 h-10 bg-gradient-to-r from-blue-500 to-indigo-600 rounded-xl flex items-center justify-center">
|
||||
<span className="text-white font-bold">📊</span>
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">Dashboard</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||||
Overview of your SiliconPin platform
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats Cards */}
|
||||
<DashboardStats stats={data} />
|
||||
|
||||
{/* Service Breakdown */}
|
||||
<Card className="border border-gray-200 dark:border-gray-700 shadow-lg bg-white dark:bg-gray-800">
|
||||
<CardHeader className="pb-4">
|
||||
<CardTitle className="text-xl font-semibold text-gray-800 dark:text-white flex items-center space-x-2">
|
||||
<span>🔧</span>
|
||||
<span>Service Breakdown</span>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{data.services.breakdown.map((service, index) => {
|
||||
const colors = [
|
||||
'from-blue-500 to-blue-600',
|
||||
'from-indigo-500 to-indigo-600',
|
||||
'from-purple-500 to-purple-600',
|
||||
'from-pink-500 to-pink-600',
|
||||
'from-red-500 to-red-600',
|
||||
'from-orange-500 to-orange-600',
|
||||
'from-yellow-500 to-yellow-600',
|
||||
'from-green-500 to-green-600',
|
||||
'from-teal-500 to-teal-600',
|
||||
'from-cyan-500 to-cyan-600',
|
||||
]
|
||||
const colorClass = colors[index % colors.length]
|
||||
|
||||
return (
|
||||
<div
|
||||
key={service._id}
|
||||
className={`bg-gradient-to-br ${colorClass} p-5 rounded-xl text-white shadow-lg hover:shadow-xl transition-all duration-200 hover:scale-105`}
|
||||
>
|
||||
<h3 className="font-semibold text-white capitalize text-sm">
|
||||
{service._id.replace('_', ' ')}
|
||||
</h3>
|
||||
<p className="text-2xl font-bold text-white mt-2">{service.count}</p>
|
||||
<p className="text-sm text-white/90 mt-1">
|
||||
₹{service.revenue.toLocaleString()} revenue
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Recent Activity */}
|
||||
<RecentActivity
|
||||
recentUsers={data.recentActivity.users}
|
||||
recentTransactions={data.recentActivity.transactions}
|
||||
/>
|
||||
</div>
|
||||
</AdminLayout>
|
||||
)
|
||||
}
|
||||
19
app/admin/reports/page.tsx
Normal file
19
app/admin/reports/page.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
'use client'
|
||||
|
||||
import AdminLayout from '@/components/admin/AdminLayout'
|
||||
import ReportsManagement from '@/components/admin/ReportsManagement'
|
||||
|
||||
export default function AdminReportsPage() {
|
||||
return (
|
||||
<AdminLayout>
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">Reports & Analytics</h1>
|
||||
<p className="text-gray-600 mt-2">Generate and export comprehensive system reports</p>
|
||||
</div>
|
||||
|
||||
<ReportsManagement />
|
||||
</div>
|
||||
</AdminLayout>
|
||||
)
|
||||
}
|
||||
21
app/admin/services/page.tsx
Normal file
21
app/admin/services/page.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
'use client'
|
||||
|
||||
import AdminLayout from '@/components/admin/AdminLayout'
|
||||
import ServiceManagement from '@/components/admin/ServiceManagement'
|
||||
|
||||
export default function AdminServicesPage() {
|
||||
return (
|
||||
<AdminLayout>
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">Service Management</h1>
|
||||
<p className="text-gray-600 mt-2">
|
||||
Manage deployments, developer requests, and service status
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ServiceManagement />
|
||||
</div>
|
||||
</AdminLayout>
|
||||
)
|
||||
}
|
||||
19
app/admin/settings/page.tsx
Normal file
19
app/admin/settings/page.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
'use client'
|
||||
|
||||
import AdminLayout from '@/components/admin/AdminLayout'
|
||||
import SystemSettings from '@/components/admin/SystemSettings'
|
||||
|
||||
export default function AdminSettingsPage() {
|
||||
return (
|
||||
<AdminLayout>
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">System Settings</h1>
|
||||
<p className="text-gray-600 mt-2">Configure system-wide settings and preferences</p>
|
||||
</div>
|
||||
|
||||
<SystemSettings />
|
||||
</div>
|
||||
</AdminLayout>
|
||||
)
|
||||
}
|
||||
19
app/admin/users/page.tsx
Normal file
19
app/admin/users/page.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
'use client'
|
||||
|
||||
import AdminLayout from '@/components/admin/AdminLayout'
|
||||
import UserManagement from '@/components/admin/UserManagement'
|
||||
|
||||
export default function AdminUsersPage() {
|
||||
return (
|
||||
<AdminLayout>
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">User Management</h1>
|
||||
<p className="text-gray-600 mt-2">Manage users, roles, and permissions</p>
|
||||
</div>
|
||||
|
||||
<UserManagement />
|
||||
</div>
|
||||
</AdminLayout>
|
||||
)
|
||||
}
|
||||
183
app/api/admin/billing/route.ts
Normal file
183
app/api/admin/billing/route.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { withAdminAuth } from '@/lib/admin-middleware'
|
||||
import { connectDB } from '@/lib/mongodb'
|
||||
import { Billing } from '@/models/billing'
|
||||
import { Transaction } from '@/models/transaction'
|
||||
import { z } from 'zod'
|
||||
|
||||
const BillingUpdateSchema = z.object({
|
||||
payment_status: z.enum(['pending', 'completed', 'failed', 'refunded']).optional(),
|
||||
service_status: z.enum(['active', 'inactive', 'suspended', 'cancelled']).optional(),
|
||||
amount: z.number().min(0).optional(),
|
||||
notes: z.string().optional(),
|
||||
})
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
return withAdminAuth(request, async (req, admin) => {
|
||||
try {
|
||||
await connectDB()
|
||||
|
||||
const { searchParams } = new URL(request.url)
|
||||
const page = parseInt(searchParams.get('page') || '1')
|
||||
const limit = parseInt(searchParams.get('limit') || '20')
|
||||
const search = searchParams.get('search') || ''
|
||||
const serviceType = searchParams.get('serviceType') || ''
|
||||
const paymentStatus = searchParams.get('paymentStatus') || ''
|
||||
const dateFrom = searchParams.get('dateFrom')
|
||||
const dateTo = searchParams.get('dateTo')
|
||||
|
||||
const skip = (page - 1) * limit
|
||||
|
||||
// Build filter query
|
||||
const filter: any = {}
|
||||
|
||||
if (search) {
|
||||
filter.$or = [
|
||||
{ user_email: { $regex: search, $options: 'i' } },
|
||||
{ silicon_id: { $regex: search, $options: 'i' } },
|
||||
{ service_name: { $regex: search, $options: 'i' } },
|
||||
]
|
||||
}
|
||||
|
||||
if (serviceType && serviceType !== 'all') {
|
||||
filter.service_type = serviceType
|
||||
}
|
||||
|
||||
if (paymentStatus && paymentStatus !== 'all') {
|
||||
filter.payment_status = paymentStatus
|
||||
}
|
||||
|
||||
if (dateFrom || dateTo) {
|
||||
filter.created_at = {}
|
||||
if (dateFrom) filter.created_at.$gte = new Date(dateFrom)
|
||||
if (dateTo) filter.created_at.$lte = new Date(dateTo)
|
||||
}
|
||||
|
||||
const [billings, totalBillings] = await Promise.all([
|
||||
Billing.find(filter).sort({ created_at: -1 }).skip(skip).limit(limit),
|
||||
Billing.countDocuments(filter),
|
||||
])
|
||||
|
||||
const totalPages = Math.ceil(totalBillings / limit)
|
||||
|
||||
// Calculate summary statistics for current filter
|
||||
const summaryStats = await Billing.aggregate([
|
||||
{ $match: filter },
|
||||
{
|
||||
$group: {
|
||||
_id: null,
|
||||
totalAmount: { $sum: '$amount' },
|
||||
totalTax: { $sum: '$tax_amount' },
|
||||
totalDiscount: { $sum: '$discount_applied' },
|
||||
completedCount: {
|
||||
$sum: { $cond: [{ $eq: ['$payment_status', 'completed'] }, 1, 0] },
|
||||
},
|
||||
pendingCount: {
|
||||
$sum: { $cond: [{ $eq: ['$payment_status', 'pending'] }, 1, 0] },
|
||||
},
|
||||
failedCount: {
|
||||
$sum: { $cond: [{ $eq: ['$payment_status', 'failed'] }, 1, 0] },
|
||||
},
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
return NextResponse.json({
|
||||
billings,
|
||||
pagination: {
|
||||
currentPage: page,
|
||||
totalPages,
|
||||
totalBillings,
|
||||
hasNext: page < totalPages,
|
||||
hasPrev: page > 1,
|
||||
},
|
||||
summary: summaryStats[0] || {
|
||||
totalAmount: 0,
|
||||
totalTax: 0,
|
||||
totalDiscount: 0,
|
||||
completedCount: 0,
|
||||
pendingCount: 0,
|
||||
failedCount: 0,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Admin billing fetch error:', error)
|
||||
return NextResponse.json({ error: 'Failed to fetch billing data' }, { status: 500 })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
return withAdminAuth(request, async (req, admin) => {
|
||||
try {
|
||||
await connectDB()
|
||||
|
||||
const body = await request.json()
|
||||
const { action, billingId, data } = body
|
||||
|
||||
if (action === 'update') {
|
||||
const validatedData = BillingUpdateSchema.parse(data)
|
||||
|
||||
const billing = await Billing.findByIdAndUpdate(
|
||||
billingId,
|
||||
{
|
||||
...validatedData,
|
||||
updated_at: new Date(),
|
||||
},
|
||||
{ new: true, runValidators: true }
|
||||
)
|
||||
|
||||
if (!billing) {
|
||||
return NextResponse.json({ error: 'Billing record not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
return NextResponse.json({ billing })
|
||||
}
|
||||
|
||||
if (action === 'refund') {
|
||||
const billing = await Billing.findById(billingId)
|
||||
|
||||
if (!billing) {
|
||||
return NextResponse.json({ error: 'Billing record not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
if (billing.payment_status !== 'paid') {
|
||||
return NextResponse.json({ error: 'Can only refund paid payments' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Update billing status
|
||||
billing.payment_status = 'refunded'
|
||||
billing.status = 'cancelled'
|
||||
await billing.save()
|
||||
|
||||
// Create refund transaction
|
||||
const refundTransaction = new Transaction({
|
||||
userId: billing.user_id,
|
||||
type: 'credit',
|
||||
amount: billing.amount,
|
||||
description: `Refund for ${billing.service_name}`,
|
||||
status: 'completed',
|
||||
reference: `refund_${billing._id}`,
|
||||
metadata: {
|
||||
originalBillingId: billing._id,
|
||||
refundedBy: admin.id,
|
||||
refundReason: data.refundReason || 'Admin refund',
|
||||
},
|
||||
})
|
||||
|
||||
await refundTransaction.save()
|
||||
|
||||
return NextResponse.json({
|
||||
billing,
|
||||
transaction: refundTransaction,
|
||||
message: 'Refund processed successfully',
|
||||
})
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: 'Invalid action' }, { status: 400 })
|
||||
} catch (error) {
|
||||
console.error('Admin billing action error:', error)
|
||||
return NextResponse.json({ error: 'Failed to perform billing action' }, { status: 500 })
|
||||
}
|
||||
})
|
||||
}
|
||||
155
app/api/admin/dashboard/route.ts
Normal file
155
app/api/admin/dashboard/route.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { withAdminAuth } from '@/lib/admin-middleware'
|
||||
import { connectDB } from '@/lib/mongodb'
|
||||
import { User } from '@/models/user'
|
||||
import { Transaction } from '@/models/transaction'
|
||||
import { Billing } from '@/models/billing'
|
||||
import { DeveloperRequest } from '@/models/developer-request'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
return withAdminAuth(request, async (req, admin) => {
|
||||
try {
|
||||
await connectDB()
|
||||
|
||||
// Get current date for time-based queries
|
||||
const now = new Date()
|
||||
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1)
|
||||
const startOfWeek = new Date(now.setDate(now.getDate() - now.getDay()))
|
||||
const startOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate())
|
||||
|
||||
// User statistics
|
||||
const totalUsers = await User.countDocuments()
|
||||
const activeUsers = await User.countDocuments({ lastLogin: { $gte: startOfMonth } })
|
||||
const newUsersThisMonth = await User.countDocuments({ createdAt: { $gte: startOfMonth } })
|
||||
const verifiedUsers = await User.countDocuments({ isVerified: true })
|
||||
|
||||
// Financial statistics
|
||||
const totalRevenue = await Billing.aggregate([
|
||||
{ $group: { _id: null, total: { $sum: '$amount' } } },
|
||||
])
|
||||
|
||||
const monthlyRevenue = await Billing.aggregate([
|
||||
{ $match: { created_at: { $gte: startOfMonth } } },
|
||||
{ $group: { _id: null, total: { $sum: '$amount' } } },
|
||||
])
|
||||
|
||||
const weeklyRevenue = await Billing.aggregate([
|
||||
{ $match: { created_at: { $gte: startOfWeek } } },
|
||||
{ $group: { _id: null, total: { $sum: '$amount' } } },
|
||||
])
|
||||
|
||||
// Transaction statistics
|
||||
const totalTransactions = await Transaction.countDocuments()
|
||||
const monthlyTransactions = await Transaction.countDocuments({
|
||||
createdAt: { $gte: startOfMonth },
|
||||
})
|
||||
|
||||
// Service statistics
|
||||
const totalServices = await Billing.countDocuments()
|
||||
const activeServices = await Billing.countDocuments({
|
||||
payment_status: 'paid',
|
||||
service_status: 1, // 1 = active
|
||||
})
|
||||
|
||||
// Developer requests
|
||||
const totalDeveloperRequests = await DeveloperRequest.countDocuments()
|
||||
const pendingDeveloperRequests = await DeveloperRequest.countDocuments({
|
||||
status: 'pending',
|
||||
})
|
||||
|
||||
// Recent activity (last 7 days)
|
||||
const recentUsers = await User.find({ createdAt: { $gte: startOfWeek } })
|
||||
.select('name email siliconId createdAt')
|
||||
.sort({ createdAt: -1 })
|
||||
.limit(10)
|
||||
|
||||
const recentTransactions = await Transaction.find({ createdAt: { $gte: startOfWeek } })
|
||||
.populate('userId', 'name email siliconId')
|
||||
.sort({ createdAt: -1 })
|
||||
.limit(10)
|
||||
|
||||
// Service breakdown
|
||||
const serviceBreakdown = await Billing.aggregate([
|
||||
{
|
||||
$group: {
|
||||
_id: '$service_type',
|
||||
count: { $sum: 1 },
|
||||
revenue: { $sum: '$amount' },
|
||||
},
|
||||
},
|
||||
{ $sort: { count: -1 } },
|
||||
])
|
||||
|
||||
// Monthly growth data (last 6 months)
|
||||
const sixMonthsAgo = new Date()
|
||||
sixMonthsAgo.setMonth(sixMonthsAgo.getMonth() - 6)
|
||||
|
||||
const monthlyGrowth = await User.aggregate([
|
||||
{ $match: { createdAt: { $gte: sixMonthsAgo } } },
|
||||
{
|
||||
$group: {
|
||||
_id: {
|
||||
year: { $year: '$createdAt' },
|
||||
month: { $month: '$createdAt' },
|
||||
},
|
||||
count: { $sum: 1 },
|
||||
},
|
||||
},
|
||||
{ $sort: { '_id.year': 1, '_id.month': 1 } },
|
||||
])
|
||||
|
||||
const revenueGrowth = await Billing.aggregate([
|
||||
{ $match: { created_at: { $gte: sixMonthsAgo } } },
|
||||
{
|
||||
$group: {
|
||||
_id: {
|
||||
year: { $year: '$created_at' },
|
||||
month: { $month: '$created_at' },
|
||||
},
|
||||
revenue: { $sum: '$amount' },
|
||||
},
|
||||
},
|
||||
{ $sort: { '_id.year': 1, '_id.month': 1 } },
|
||||
])
|
||||
|
||||
return NextResponse.json({
|
||||
users: {
|
||||
total: totalUsers,
|
||||
active: activeUsers,
|
||||
newThisMonth: newUsersThisMonth,
|
||||
verified: verifiedUsers,
|
||||
verificationRate: totalUsers > 0 ? ((verifiedUsers / totalUsers) * 100).toFixed(1) : 0,
|
||||
},
|
||||
revenue: {
|
||||
total: totalRevenue[0]?.total || 0,
|
||||
monthly: monthlyRevenue[0]?.total || 0,
|
||||
weekly: weeklyRevenue[0]?.total || 0,
|
||||
},
|
||||
transactions: {
|
||||
total: totalTransactions,
|
||||
monthly: monthlyTransactions,
|
||||
},
|
||||
services: {
|
||||
total: totalServices,
|
||||
active: activeServices,
|
||||
breakdown: serviceBreakdown,
|
||||
},
|
||||
developerRequests: {
|
||||
total: totalDeveloperRequests,
|
||||
pending: pendingDeveloperRequests,
|
||||
},
|
||||
recentActivity: {
|
||||
users: recentUsers,
|
||||
transactions: recentTransactions,
|
||||
},
|
||||
growth: {
|
||||
users: monthlyGrowth,
|
||||
revenue: revenueGrowth,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Admin dashboard error:', error)
|
||||
return NextResponse.json({ error: 'Failed to fetch dashboard data' }, { status: 500 })
|
||||
}
|
||||
})
|
||||
}
|
||||
187
app/api/admin/reports/route.ts
Normal file
187
app/api/admin/reports/route.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { withAdminAuth } from '@/lib/admin-middleware'
|
||||
import { connectDB } from '@/lib/mongodb'
|
||||
import { User } from '@/models/user'
|
||||
import { Transaction } from '@/models/transaction'
|
||||
import { Billing } from '@/models/billing'
|
||||
import { DeveloperRequest } from '@/models/developer-request'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
return withAdminAuth(request, async (req, admin) => {
|
||||
try {
|
||||
await connectDB()
|
||||
|
||||
const { searchParams } = new URL(request.url)
|
||||
const reportType = searchParams.get('type')
|
||||
const format = searchParams.get('format') || 'json'
|
||||
const dateFrom = searchParams.get('dateFrom')
|
||||
const dateTo = searchParams.get('dateTo')
|
||||
|
||||
// Build date filter
|
||||
const dateFilter: any = {}
|
||||
if (dateFrom || dateTo) {
|
||||
dateFilter.createdAt = {}
|
||||
if (dateFrom) dateFilter.createdAt.$gte = new Date(dateFrom)
|
||||
if (dateTo) dateFilter.createdAt.$lte = new Date(dateTo)
|
||||
}
|
||||
|
||||
let data: any = {}
|
||||
|
||||
switch (reportType) {
|
||||
case 'users':
|
||||
data = await User.find(dateFilter)
|
||||
.select('-password -refreshToken')
|
||||
.sort({ createdAt: -1 })
|
||||
break
|
||||
|
||||
case 'transactions':
|
||||
data = await Transaction.find(dateFilter)
|
||||
.populate('userId', 'name email siliconId')
|
||||
.sort({ createdAt: -1 })
|
||||
break
|
||||
|
||||
case 'billing':
|
||||
data = await Billing.find(
|
||||
dateFilter.createdAt ? { created_at: dateFilter.createdAt } : {}
|
||||
).sort({ created_at: -1 })
|
||||
break
|
||||
|
||||
case 'developer-requests':
|
||||
const devFilter = dateFilter.createdAt ? { createdAt: dateFilter.createdAt } : {}
|
||||
data = await (DeveloperRequest as any)
|
||||
.find(devFilter)
|
||||
.populate('userId', 'name email siliconId')
|
||||
.sort({ createdAt: -1 })
|
||||
break
|
||||
|
||||
case 'summary':
|
||||
// Generate comprehensive summary report
|
||||
const [users, transactions, billings, developerRequests] = await Promise.all([
|
||||
User.find(dateFilter).select('-password -refreshToken'),
|
||||
Transaction.find(dateFilter).populate('userId', 'name email siliconId'),
|
||||
Billing.find(dateFilter.createdAt ? { created_at: dateFilter.createdAt } : {}),
|
||||
(DeveloperRequest as any)
|
||||
.find(dateFilter.createdAt ? { createdAt: dateFilter.createdAt } : {})
|
||||
.populate('userId', 'name email siliconId'),
|
||||
])
|
||||
|
||||
// Calculate summary statistics
|
||||
const userStats = {
|
||||
total: users.length,
|
||||
verified: users.filter((u) => u.isVerified).length,
|
||||
admins: users.filter((u) => u.role === 'admin').length,
|
||||
totalBalance: users.reduce((sum, u) => sum + (u.balance || 0), 0),
|
||||
}
|
||||
|
||||
const transactionStats = {
|
||||
total: transactions.length,
|
||||
totalAmount: transactions.reduce((sum, t) => sum + t.amount, 0),
|
||||
credits: transactions.filter((t) => t.type === 'credit').length,
|
||||
debits: transactions.filter((t) => t.type === 'debit').length,
|
||||
}
|
||||
|
||||
const billingStats = {
|
||||
total: billings.length,
|
||||
totalRevenue: billings.reduce((sum, b) => sum + b.amount, 0),
|
||||
completed: billings.filter((b) => b.payment_status === 'paid').length,
|
||||
pending: billings.filter((b) => b.payment_status === 'pending').length,
|
||||
}
|
||||
|
||||
const developerStats = {
|
||||
total: developerRequests.length,
|
||||
pending: developerRequests.filter((d) => d.status === 'pending').length,
|
||||
inProgress: developerRequests.filter((d) => d.status === 'in_progress').length,
|
||||
completed: developerRequests.filter((d) => d.status === 'completed').length,
|
||||
}
|
||||
|
||||
data = {
|
||||
summary: {
|
||||
users: userStats,
|
||||
transactions: transactionStats,
|
||||
billing: billingStats,
|
||||
developerRequests: developerStats,
|
||||
},
|
||||
users: users.slice(0, 100), // Limit for performance
|
||||
transactions: transactions.slice(0, 100),
|
||||
billings: billings.slice(0, 100),
|
||||
developerRequests: developerRequests.slice(0, 100),
|
||||
}
|
||||
break
|
||||
|
||||
default:
|
||||
return NextResponse.json({ error: 'Invalid report type' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (format === 'csv') {
|
||||
// Convert to CSV format
|
||||
const csv = convertToCSV(data, reportType)
|
||||
|
||||
return new NextResponse(csv, {
|
||||
headers: {
|
||||
'Content-Type': 'text/csv',
|
||||
'Content-Disposition': `attachment; filename="${reportType}_report_${new Date().toISOString().split('T')[0]}.csv"`,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
reportType,
|
||||
generatedAt: new Date().toISOString(),
|
||||
generatedBy: admin.name,
|
||||
dateRange: { from: dateFrom, to: dateTo },
|
||||
data,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Admin reports error:', error)
|
||||
return NextResponse.json({ error: 'Failed to generate report' }, { status: 500 })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function convertToCSV(data: any, reportType: string): string {
|
||||
if (!Array.isArray(data)) {
|
||||
if (reportType === 'summary' && data.summary) {
|
||||
// For summary reports, create a simple CSV with key metrics
|
||||
const lines = [
|
||||
'Metric,Value',
|
||||
`Total Users,${data.summary.users.total}`,
|
||||
`Verified Users,${data.summary.users.verified}`,
|
||||
`Admin Users,${data.summary.users.admins}`,
|
||||
`Total Balance,${data.summary.users.totalBalance}`,
|
||||
`Total Transactions,${data.summary.transactions.total}`,
|
||||
`Transaction Amount,${data.summary.transactions.totalAmount}`,
|
||||
`Total Billing Records,${data.summary.billing.total}`,
|
||||
`Total Revenue,${data.summary.billing.totalRevenue}`,
|
||||
`Developer Requests,${data.summary.developerRequests.total}`,
|
||||
]
|
||||
return lines.join('\n')
|
||||
}
|
||||
return 'No data available'
|
||||
}
|
||||
|
||||
if (data.length === 0) {
|
||||
return 'No data available'
|
||||
}
|
||||
|
||||
// Get headers from first object
|
||||
const headers = Object.keys(data[0]).filter(
|
||||
(key) => typeof data[0][key] !== 'object' || data[0][key] === null
|
||||
)
|
||||
|
||||
// Create CSV content
|
||||
const csvHeaders = headers.join(',')
|
||||
const csvRows = data.map((row) =>
|
||||
headers
|
||||
.map((header) => {
|
||||
const value = row[header]
|
||||
if (value === null || value === undefined) return ''
|
||||
if (typeof value === 'string' && value.includes(',')) {
|
||||
return `"${value.replace(/"/g, '""')}"`
|
||||
}
|
||||
return value
|
||||
})
|
||||
.join(',')
|
||||
)
|
||||
|
||||
return [csvHeaders, ...csvRows].join('\n')
|
||||
}
|
||||
200
app/api/admin/services/route.ts
Normal file
200
app/api/admin/services/route.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { withAdminAuth } from '@/lib/admin-middleware'
|
||||
import { connectDB } from '@/lib/mongodb'
|
||||
import { Billing } from '@/models/billing'
|
||||
import { DeveloperRequest } from '@/models/developer-request'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
return withAdminAuth(request, async (req, admin) => {
|
||||
try {
|
||||
await connectDB()
|
||||
|
||||
const { searchParams } = new URL(request.url)
|
||||
const page = parseInt(searchParams.get('page') || '1')
|
||||
const limit = parseInt(searchParams.get('limit') || '20')
|
||||
const serviceType = searchParams.get('serviceType') || ''
|
||||
const status = searchParams.get('status') || ''
|
||||
const dateFrom = searchParams.get('dateFrom')
|
||||
const dateTo = searchParams.get('dateTo')
|
||||
|
||||
const skip = (page - 1) * limit
|
||||
|
||||
// Build filter query for billing services
|
||||
const billingFilter: any = {}
|
||||
|
||||
if (serviceType && serviceType !== 'all') {
|
||||
billingFilter.service_type = serviceType
|
||||
}
|
||||
|
||||
if (status && status !== 'all') {
|
||||
billingFilter.service_status = status
|
||||
}
|
||||
|
||||
if (dateFrom || dateTo) {
|
||||
billingFilter.created_at = {}
|
||||
if (dateFrom) billingFilter.created_at.$gte = new Date(dateFrom)
|
||||
if (dateTo) billingFilter.created_at.$lte = new Date(dateTo)
|
||||
}
|
||||
|
||||
// Fetch billing services
|
||||
const [billingServices, totalBillingServices] = await Promise.all([
|
||||
Billing.find(billingFilter).sort({ created_at: -1 }).skip(skip).limit(limit),
|
||||
Billing.countDocuments(billingFilter),
|
||||
])
|
||||
|
||||
// Build filter for developer requests
|
||||
const devRequestFilter: any = {}
|
||||
|
||||
if (status && status !== 'all') {
|
||||
devRequestFilter.status = status
|
||||
}
|
||||
|
||||
if (dateFrom || dateTo) {
|
||||
devRequestFilter.createdAt = {}
|
||||
if (dateFrom) devRequestFilter.createdAt.$gte = new Date(dateFrom)
|
||||
if (dateTo) devRequestFilter.createdAt.$lte = new Date(dateTo)
|
||||
}
|
||||
|
||||
// Fetch developer requests
|
||||
const [developerRequests, totalDeveloperRequests] = await Promise.all([
|
||||
(DeveloperRequest as any)
|
||||
.find(devRequestFilter)
|
||||
.populate('userId', 'name email siliconId')
|
||||
.sort({ createdAt: -1 })
|
||||
.skip(skip)
|
||||
.limit(limit),
|
||||
(DeveloperRequest as any).countDocuments(devRequestFilter),
|
||||
])
|
||||
|
||||
// Service statistics
|
||||
const serviceStats = await Billing.aggregate([
|
||||
{
|
||||
$group: {
|
||||
_id: '$service_type',
|
||||
count: { $sum: 1 },
|
||||
revenue: { $sum: '$amount' },
|
||||
activeServices: {
|
||||
$sum: { $cond: [{ $eq: ['$service_status', 'active'] }, 1, 0] },
|
||||
},
|
||||
},
|
||||
},
|
||||
{ $sort: { count: -1 } },
|
||||
])
|
||||
|
||||
// Status breakdown
|
||||
const statusBreakdown = await Billing.aggregate([
|
||||
{
|
||||
$group: {
|
||||
_id: '$service_status',
|
||||
count: { $sum: 1 },
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
const totalPages = Math.ceil(Math.max(totalBillingServices, totalDeveloperRequests) / limit)
|
||||
|
||||
return NextResponse.json({
|
||||
billingServices,
|
||||
developerRequests,
|
||||
serviceStats,
|
||||
statusBreakdown,
|
||||
pagination: {
|
||||
currentPage: page,
|
||||
totalPages,
|
||||
totalBillingServices,
|
||||
totalDeveloperRequests,
|
||||
hasNext: page < totalPages,
|
||||
hasPrev: page > 1,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Admin services fetch error:', error)
|
||||
return NextResponse.json({ error: 'Failed to fetch services data' }, { status: 500 })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
return withAdminAuth(request, async (req, admin) => {
|
||||
try {
|
||||
await connectDB()
|
||||
|
||||
const body = await request.json()
|
||||
const { action, serviceId, serviceType, data } = body
|
||||
|
||||
if (action === 'updateBilling') {
|
||||
const billing = await Billing.findByIdAndUpdate(
|
||||
serviceId,
|
||||
{
|
||||
service_status: data.service_status,
|
||||
updated_at: new Date(),
|
||||
},
|
||||
{ new: true }
|
||||
)
|
||||
|
||||
if (!billing) {
|
||||
return NextResponse.json({ error: 'Service not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
return NextResponse.json({ service: billing })
|
||||
}
|
||||
|
||||
if (action === 'updateDeveloperRequest') {
|
||||
const developerRequest = await (DeveloperRequest as any)
|
||||
.findByIdAndUpdate(
|
||||
serviceId,
|
||||
{
|
||||
status: data.status,
|
||||
assignedDeveloper: data.assignedDeveloper,
|
||||
estimatedCompletionDate: data.estimatedCompletionDate,
|
||||
notes: data.notes,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
{ new: true }
|
||||
)
|
||||
.populate('userId', 'name email siliconId')
|
||||
|
||||
if (!developerRequest) {
|
||||
return NextResponse.json({ error: 'Developer request not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
return NextResponse.json({ service: developerRequest })
|
||||
}
|
||||
|
||||
if (action === 'cancelService') {
|
||||
if (serviceType === 'billing') {
|
||||
const billing = await Billing.findByIdAndUpdate(
|
||||
serviceId,
|
||||
{
|
||||
service_status: 'cancelled',
|
||||
updated_at: new Date(),
|
||||
},
|
||||
{ new: true }
|
||||
)
|
||||
|
||||
return NextResponse.json({ service: billing })
|
||||
}
|
||||
|
||||
if (serviceType === 'developer') {
|
||||
const updatedDeveloperRequest = await (DeveloperRequest as any)
|
||||
.findByIdAndUpdate(
|
||||
serviceId,
|
||||
{
|
||||
status: 'cancelled',
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
{ new: true }
|
||||
)
|
||||
.populate('userId', 'name email siliconId')
|
||||
|
||||
return NextResponse.json({ service: updatedDeveloperRequest })
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: 'Invalid action or service type' }, { status: 400 })
|
||||
} catch (error) {
|
||||
console.error('Admin service action error:', error)
|
||||
return NextResponse.json({ error: 'Failed to perform service action' }, { status: 500 })
|
||||
}
|
||||
})
|
||||
}
|
||||
117
app/api/admin/settings/route.ts
Normal file
117
app/api/admin/settings/route.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { withAdminAuth } from '@/lib/admin-middleware'
|
||||
import { connectDB } from '@/lib/mongodb'
|
||||
import { User } from '@/models/user'
|
||||
import { z } from 'zod'
|
||||
import { getSystemSettings, updateSystemSettings } from '@/lib/system-settings'
|
||||
|
||||
const SystemSettingsSchema = z.object({
|
||||
maintenanceMode: z.boolean().optional(),
|
||||
registrationEnabled: z.boolean().optional(),
|
||||
emailVerificationRequired: z.boolean().optional(),
|
||||
maxUserBalance: z.number().min(0).optional(),
|
||||
defaultUserRole: z.enum(['user', 'admin']).optional(),
|
||||
systemMessage: z.string().optional(),
|
||||
paymentGatewayEnabled: z.boolean().optional(),
|
||||
developerHireEnabled: z.boolean().optional(),
|
||||
vpsDeploymentEnabled: z.boolean().optional(),
|
||||
kubernetesDeploymentEnabled: z.boolean().optional(),
|
||||
vpnServiceEnabled: z.boolean().optional(),
|
||||
})
|
||||
|
||||
// System settings are now managed by the system-settings service
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
return withAdminAuth(request, async (req, admin) => {
|
||||
try {
|
||||
await connectDB()
|
||||
|
||||
// Get system statistics
|
||||
const totalUsers = await User.countDocuments()
|
||||
const adminUsers = await User.countDocuments({ role: 'admin' })
|
||||
const verifiedUsers = await User.countDocuments({ isVerified: true })
|
||||
const unverifiedUsers = await User.countDocuments({ isVerified: false })
|
||||
|
||||
// Get recent admin activities (mock data for now)
|
||||
const recentActivities = [
|
||||
{
|
||||
id: '1',
|
||||
action: 'User role updated',
|
||||
details: 'Changed user role from user to admin',
|
||||
timestamp: new Date().toISOString(),
|
||||
adminName: admin.name,
|
||||
},
|
||||
]
|
||||
|
||||
return NextResponse.json({
|
||||
settings: await getSystemSettings(),
|
||||
statistics: {
|
||||
totalUsers,
|
||||
adminUsers,
|
||||
verifiedUsers,
|
||||
unverifiedUsers,
|
||||
},
|
||||
recentActivities,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Admin settings fetch error:', error)
|
||||
return NextResponse.json({ error: 'Failed to fetch system settings' }, { status: 500 })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
return withAdminAuth(request, async (req, admin) => {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { action, settings } = body
|
||||
|
||||
if (action === 'updateSettings') {
|
||||
const validatedSettings = SystemSettingsSchema.parse(settings)
|
||||
|
||||
// Update system settings
|
||||
const updatedSettings = {
|
||||
...validatedSettings,
|
||||
lastUpdated: new Date().toISOString(),
|
||||
updatedBy: admin.name,
|
||||
}
|
||||
await updateSystemSettings(updatedSettings)
|
||||
|
||||
return NextResponse.json({
|
||||
settings: await getSystemSettings(),
|
||||
message: 'Settings updated successfully',
|
||||
})
|
||||
}
|
||||
|
||||
if (action === 'clearCache') {
|
||||
// Mock cache clearing - in a real app, this would clear Redis/memory cache
|
||||
return NextResponse.json({
|
||||
message: 'System cache cleared successfully',
|
||||
})
|
||||
}
|
||||
|
||||
if (action === 'backupDatabase') {
|
||||
// Mock database backup - in a real app, this would trigger a backup process
|
||||
return NextResponse.json({
|
||||
message: 'Database backup initiated successfully',
|
||||
backupId: `backup_${Date.now()}`,
|
||||
})
|
||||
}
|
||||
|
||||
if (action === 'sendSystemNotification') {
|
||||
const { message, targetUsers } = body
|
||||
|
||||
// Mock system notification - in a real app, this would send notifications
|
||||
return NextResponse.json({
|
||||
message: `System notification sent to ${targetUsers} users`,
|
||||
notificationId: `notif_${Date.now()}`,
|
||||
})
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: 'Invalid action' }, { status: 400 })
|
||||
} catch (error) {
|
||||
console.error('Admin settings update error:', error)
|
||||
return NextResponse.json({ error: 'Failed to update system settings' }, { status: 500 })
|
||||
}
|
||||
})
|
||||
}
|
||||
163
app/api/admin/users/route.ts
Normal file
163
app/api/admin/users/route.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { withAdminAuth } from '@/lib/admin-middleware'
|
||||
import { connectDB } from '@/lib/mongodb'
|
||||
import { User } from '@/models/user'
|
||||
import { z } from 'zod'
|
||||
|
||||
const UserUpdateSchema = z.object({
|
||||
name: z.string().min(1).optional(),
|
||||
email: z.string().email().optional(),
|
||||
role: z.enum(['user', 'admin']).optional(),
|
||||
isVerified: z.boolean().optional(),
|
||||
balance: z.number().min(0).optional(),
|
||||
})
|
||||
|
||||
const UserCreateSchema = z.object({
|
||||
name: z.string().min(1),
|
||||
email: z.string().email(),
|
||||
password: z.string().min(6),
|
||||
role: z.enum(['user', 'admin']).default('user'),
|
||||
isVerified: z.boolean().default(false),
|
||||
balance: z.number().min(0).default(0),
|
||||
})
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
return withAdminAuth(request, async (req, admin) => {
|
||||
try {
|
||||
await connectDB()
|
||||
|
||||
const { searchParams } = new URL(request.url)
|
||||
const page = parseInt(searchParams.get('page') || '1')
|
||||
const limit = parseInt(searchParams.get('limit') || '20')
|
||||
const search = searchParams.get('search') || ''
|
||||
const role = searchParams.get('role') || ''
|
||||
const verified = searchParams.get('verified') || ''
|
||||
|
||||
const skip = (page - 1) * limit
|
||||
|
||||
// Build filter query
|
||||
const filter: any = {}
|
||||
|
||||
if (search) {
|
||||
filter.$or = [
|
||||
{ name: { $regex: search, $options: 'i' } },
|
||||
{ email: { $regex: search, $options: 'i' } },
|
||||
{ siliconId: { $regex: search, $options: 'i' } },
|
||||
]
|
||||
}
|
||||
|
||||
if (role && role !== 'all') {
|
||||
filter.role = role
|
||||
}
|
||||
|
||||
if (verified && verified !== 'all') {
|
||||
filter.isVerified = verified === 'true'
|
||||
}
|
||||
|
||||
const [users, totalUsers] = await Promise.all([
|
||||
User.find(filter)
|
||||
.select('-password -refreshToken')
|
||||
.sort({ createdAt: -1 })
|
||||
.skip(skip)
|
||||
.limit(limit),
|
||||
User.countDocuments(filter),
|
||||
])
|
||||
|
||||
const totalPages = Math.ceil(totalUsers / limit)
|
||||
|
||||
return NextResponse.json({
|
||||
users,
|
||||
pagination: {
|
||||
currentPage: page,
|
||||
totalPages,
|
||||
totalUsers,
|
||||
hasNext: page < totalPages,
|
||||
hasPrev: page > 1,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Admin users fetch error:', error)
|
||||
return NextResponse.json({ error: 'Failed to fetch users' }, { status: 500 })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
return withAdminAuth(request, async (req, admin) => {
|
||||
try {
|
||||
await connectDB()
|
||||
|
||||
const body = await request.json()
|
||||
const { action, userId, data } = body
|
||||
|
||||
if (action === 'update') {
|
||||
const validatedData = UserUpdateSchema.parse(data)
|
||||
|
||||
const user = await User.findByIdAndUpdate(userId, validatedData, {
|
||||
new: true,
|
||||
runValidators: true,
|
||||
}).select('-password -refreshToken')
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
return NextResponse.json({ user })
|
||||
}
|
||||
|
||||
if (action === 'create') {
|
||||
const validatedData = UserCreateSchema.parse(data)
|
||||
|
||||
// Check if user already exists
|
||||
const existingUser = await User.findOne({ email: validatedData.email })
|
||||
if (existingUser) {
|
||||
return NextResponse.json(
|
||||
{ error: 'User with this email already exists' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Generate unique Silicon ID
|
||||
const generateSiliconId = () => {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
|
||||
let result = 'SP'
|
||||
for (let i = 0; i < 8; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
let siliconId = generateSiliconId()
|
||||
while (await User.findOne({ siliconId })) {
|
||||
siliconId = generateSiliconId()
|
||||
}
|
||||
|
||||
const user = new User({
|
||||
...validatedData,
|
||||
siliconId,
|
||||
provider: 'local',
|
||||
})
|
||||
|
||||
await user.save()
|
||||
|
||||
const userResponse = await User.findById(user._id).select('-password -refreshToken')
|
||||
return NextResponse.json({ user: userResponse })
|
||||
}
|
||||
|
||||
if (action === 'delete') {
|
||||
const user = await User.findByIdAndDelete(userId)
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
return NextResponse.json({ message: 'User deleted successfully' })
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: 'Invalid action' }, { status: 400 })
|
||||
} catch (error) {
|
||||
console.error('Admin user action error:', error)
|
||||
return NextResponse.json({ error: 'Failed to perform user action' }, { status: 500 })
|
||||
}
|
||||
})
|
||||
}
|
||||
120
app/api/auth/forgot-password/route.ts
Normal file
120
app/api/auth/forgot-password/route.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
|
||||
const ForgotPasswordSchema = z.object({
|
||||
email: z.string().email('Please enter a valid email address'),
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
|
||||
// Validate the request body
|
||||
const validatedData = ForgotPasswordSchema.parse(body)
|
||||
|
||||
// Simulate processing delay
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
// Dummy API - Always return an error for demonstration
|
||||
// You can change this behavior for testing different scenarios
|
||||
|
||||
const email = validatedData.email
|
||||
|
||||
// Simulate different error scenarios based on email
|
||||
if (email === 'test@example.com') {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Email address not found in our system',
|
||||
code: 'EMAIL_NOT_FOUND'
|
||||
}
|
||||
},
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
if (email.includes('blocked')) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: 'This email address has been temporarily blocked',
|
||||
code: 'EMAIL_BLOCKED'
|
||||
}
|
||||
},
|
||||
{ status: 429 }
|
||||
)
|
||||
}
|
||||
|
||||
if (email.includes('invalid')) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Invalid email format',
|
||||
code: 'INVALID_EMAIL'
|
||||
}
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Default error response (500 Internal Server Error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Unable to process password reset request at this time. Please try again later.',
|
||||
code: 'SERVER_ERROR'
|
||||
}
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
|
||||
// Uncomment below for success response (when you want to test success state)
|
||||
/*
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
message: 'Password reset email sent successfully',
|
||||
data: {
|
||||
email: validatedData.email,
|
||||
resetTokenExpiry: Date.now() + 3600000 // 1 hour from now
|
||||
}
|
||||
},
|
||||
{ status: 200 }
|
||||
)
|
||||
*/
|
||||
|
||||
} catch (error) {
|
||||
console.error('Forgot password API error:', error)
|
||||
|
||||
// Handle validation errors
|
||||
if (error instanceof z.ZodError) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Invalid request data',
|
||||
code: 'VALIDATION_ERROR',
|
||||
details: error.issues
|
||||
}
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Handle other errors
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: 'An unexpected error occurred',
|
||||
code: 'INTERNAL_ERROR'
|
||||
}
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
113
app/api/auth/google/callback/route.ts
Normal file
113
app/api/auth/google/callback/route.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getGoogleUser } from '@/lib/google-oauth'
|
||||
import { generateTokens } from '@/lib/jwt'
|
||||
import connectDB from '@/lib/mongodb'
|
||||
import { User } from '@/models/user'
|
||||
import { appConfig } from '@/lib/env'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const code = searchParams.get('code')
|
||||
const state = searchParams.get('state')
|
||||
const error = searchParams.get('error')
|
||||
|
||||
// Handle OAuth errors
|
||||
if (error) {
|
||||
return NextResponse.redirect(
|
||||
new URL(`/auth?error=${encodeURIComponent(error)}`, appConfig.appUrl)
|
||||
)
|
||||
}
|
||||
|
||||
// Validate required parameters
|
||||
if (!code || state !== 'google_oauth') {
|
||||
return NextResponse.redirect(new URL('/auth?error=invalid_oauth_callback', appConfig.appUrl))
|
||||
}
|
||||
|
||||
// Get user info from Google
|
||||
const googleUser = await getGoogleUser(code)
|
||||
|
||||
// Connect to database
|
||||
await connectDB()
|
||||
|
||||
// Check if user exists
|
||||
let user = await User.findOne({
|
||||
$or: [
|
||||
{ email: googleUser.email.toLowerCase() },
|
||||
{ providerId: googleUser.id, provider: 'google' },
|
||||
],
|
||||
})
|
||||
|
||||
if (!user) {
|
||||
// Create new user
|
||||
user = new User({
|
||||
email: googleUser.email.toLowerCase(),
|
||||
name: googleUser.name,
|
||||
provider: 'google',
|
||||
providerId: googleUser.id,
|
||||
avatar: googleUser.picture || undefined,
|
||||
isVerified: googleUser.verified_email,
|
||||
lastLogin: new Date(),
|
||||
})
|
||||
} else {
|
||||
// Update existing user with Google info (preserve original provider)
|
||||
// Only link Google if user doesn't already have a local account
|
||||
if (user.provider !== 'local') {
|
||||
user.provider = 'google'
|
||||
user.providerId = googleUser.id
|
||||
} else {
|
||||
// For local users, just add Google info without changing provider
|
||||
if (!user.providerId) {
|
||||
user.providerId = googleUser.id
|
||||
}
|
||||
}
|
||||
|
||||
// Update other info safely
|
||||
user.name = googleUser.name
|
||||
user.isVerified = googleUser.verified_email || user.isVerified
|
||||
user.lastLogin = new Date()
|
||||
|
||||
// Update avatar only if user doesn't have one
|
||||
if (googleUser.picture && !user.avatar) {
|
||||
user.avatar = googleUser.picture
|
||||
}
|
||||
}
|
||||
|
||||
// Generate tokens
|
||||
const { accessToken, refreshToken } = generateTokens({
|
||||
userId: user._id.toString(),
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
})
|
||||
|
||||
// Update user's refresh token
|
||||
user.refreshToken = refreshToken
|
||||
await user.save()
|
||||
|
||||
// Create redirect response using the public app URL
|
||||
const redirectURL = new URL('/dashboard', appConfig.appUrl)
|
||||
const response = NextResponse.redirect(redirectURL)
|
||||
|
||||
// Set HTTP-only cookies
|
||||
response.cookies.set('accessToken', accessToken, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
maxAge: 15 * 60, // 15 minutes
|
||||
path: '/',
|
||||
})
|
||||
|
||||
response.cookies.set('refreshToken', refreshToken, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
maxAge: 7 * 24 * 60 * 60, // 7 days
|
||||
path: '/',
|
||||
})
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('Google OAuth callback error:', error)
|
||||
return NextResponse.redirect(new URL('/auth?error=oauth_callback_failed', appConfig.appUrl))
|
||||
}
|
||||
}
|
||||
22
app/api/auth/google/route.ts
Normal file
22
app/api/auth/google/route.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getGoogleAuthURL } from '@/lib/google-oauth'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const authURL = getGoogleAuthURL()
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: { authURL },
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Google OAuth URL generation error:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: { message: 'Failed to generate Google auth URL', code: 'OAUTH_ERROR' },
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
98
app/api/auth/login/route.ts
Normal file
98
app/api/auth/login/route.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import connectDB from '@/lib/mongodb'
|
||||
import { User } from '@/models/user'
|
||||
import { generateTokens } from '@/lib/jwt'
|
||||
|
||||
const LoginSchema = z.object({
|
||||
emailOrId: z.string().min(1, 'Email or Silicon ID is required'),
|
||||
password: z.string().min(1, 'Password is required'),
|
||||
rememberMe: z.boolean().optional(),
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
|
||||
// Validate input
|
||||
const validatedData = LoginSchema.parse(body)
|
||||
|
||||
// Connect to database
|
||||
await connectDB()
|
||||
|
||||
// Find user by email or Silicon ID
|
||||
const emailOrId = validatedData.emailOrId
|
||||
const user = await User.findOne({
|
||||
$or: [{ email: emailOrId.toLowerCase() }, { siliconId: emailOrId }],
|
||||
})
|
||||
if (!user) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: { message: 'Invalid credentials', code: 'INVALID_CREDENTIALS' } },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
// Check password
|
||||
const isPasswordValid = await user.comparePassword(validatedData.password)
|
||||
if (!isPasswordValid) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: { message: 'Invalid credentials', code: 'INVALID_CREDENTIALS' } },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
// Generate tokens
|
||||
const { accessToken, refreshToken } = generateTokens({
|
||||
userId: user._id.toString(),
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
})
|
||||
|
||||
// Update user's refresh token and last login
|
||||
user.refreshToken = refreshToken
|
||||
user.lastLogin = new Date()
|
||||
await user.save()
|
||||
|
||||
// Create response with tokens
|
||||
const response = NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
user: user.toJSON(),
|
||||
accessToken,
|
||||
},
|
||||
})
|
||||
|
||||
// Set HTTP-only cookies
|
||||
response.cookies.set('accessToken', accessToken, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
maxAge: 15 * 60, // 15 minutes
|
||||
path: '/',
|
||||
})
|
||||
|
||||
response.cookies.set('refreshToken', refreshToken, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
maxAge: 7 * 24 * 60 * 60, // 7 days
|
||||
path: '/',
|
||||
})
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('Login error:', error)
|
||||
|
||||
if (error instanceof z.ZodError) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: { message: error.issues[0].message, code: 'VALIDATION_ERROR' } },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{ success: false, error: { message: 'Internal server error', code: 'INTERNAL_ERROR' } },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
62
app/api/auth/logout/route.ts
Normal file
62
app/api/auth/logout/route.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import connectDB from '@/lib/mongodb'
|
||||
import { User } from '@/models/user'
|
||||
import { verifyRefreshToken } from '@/lib/jwt'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
// Get refresh token from cookie
|
||||
const refreshToken = request.cookies.get('refreshToken')?.value
|
||||
|
||||
if (refreshToken) {
|
||||
// Verify and decode the refresh token to get user ID
|
||||
const payload = verifyRefreshToken(refreshToken)
|
||||
|
||||
if (payload) {
|
||||
// Connect to database and remove refresh token
|
||||
await connectDB()
|
||||
await User.findByIdAndUpdate(payload.userId, {
|
||||
$unset: { refreshToken: 1 },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Create response
|
||||
const response = NextResponse.json({
|
||||
success: true,
|
||||
data: { message: 'Logged out successfully' },
|
||||
})
|
||||
|
||||
// Clear cookies
|
||||
response.cookies.set('accessToken', '', {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
maxAge: 0,
|
||||
path: '/',
|
||||
})
|
||||
|
||||
response.cookies.set('refreshToken', '', {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
maxAge: 0,
|
||||
path: '/',
|
||||
})
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('Logout error:', error)
|
||||
|
||||
// Even if there's an error, we should still clear the cookies
|
||||
const response = NextResponse.json({
|
||||
success: true,
|
||||
data: { message: 'Logged out successfully' },
|
||||
})
|
||||
|
||||
response.cookies.set('accessToken', '', { maxAge: 0, path: '/' })
|
||||
response.cookies.set('refreshToken', '', { maxAge: 0, path: '/' })
|
||||
|
||||
return response
|
||||
}
|
||||
}
|
||||
33
app/api/auth/me/route.ts
Normal file
33
app/api/auth/me/route.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { withAuth } from '@/lib/auth-middleware'
|
||||
import connectDB from '@/lib/mongodb'
|
||||
import { User } from '@/models/user'
|
||||
|
||||
export const GET = withAuth(async (request: NextRequest & { user?: any }) => {
|
||||
try {
|
||||
// Connect to database
|
||||
await connectDB()
|
||||
|
||||
// Get user details
|
||||
const user = await User.findById(request.user.userId).select('-password -refreshToken')
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: { message: 'User not found', code: 'USER_NOT_FOUND' } },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: { user: user.toJSON() },
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Get user info error:', error)
|
||||
|
||||
return NextResponse.json(
|
||||
{ success: false, error: { message: 'Internal server error', code: 'INTERNAL_ERROR' } },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
99
app/api/auth/refresh/route.ts
Normal file
99
app/api/auth/refresh/route.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import connectDB from '@/lib/mongodb'
|
||||
import { User } from '@/models/user'
|
||||
import { verifyRefreshToken, generateTokens } from '@/lib/jwt'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
// Get refresh token from cookie
|
||||
const refreshToken = request.cookies.get('refreshToken')?.value
|
||||
|
||||
if (!refreshToken) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: { message: 'No refresh token provided', code: 'NO_REFRESH_TOKEN' },
|
||||
},
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
// Verify refresh token
|
||||
const payload = verifyRefreshToken(refreshToken)
|
||||
if (!payload) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: { message: 'Invalid refresh token', code: 'INVALID_REFRESH_TOKEN' },
|
||||
},
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
// Connect to database and find user
|
||||
await connectDB()
|
||||
const user = await User.findById(payload.userId)
|
||||
|
||||
// Check if user exists and refresh token matches
|
||||
if (!user) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: { message: 'User not found', code: 'USER_NOT_FOUND' } },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
// Verify the stored refresh token matches (both are JWT tokens, so direct comparison is valid)
|
||||
if (user.refreshToken !== refreshToken) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: { message: 'Refresh token mismatch', code: 'TOKEN_MISMATCH' } },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
// Generate new tokens
|
||||
const { accessToken, refreshToken: newRefreshToken } = generateTokens({
|
||||
userId: user._id.toString(),
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
})
|
||||
|
||||
// Update user's refresh token
|
||||
user.refreshToken = newRefreshToken
|
||||
await user.save()
|
||||
|
||||
// Create response with new tokens
|
||||
const response = NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
accessToken,
|
||||
user: user.toJSON(),
|
||||
},
|
||||
})
|
||||
|
||||
// Set new cookies
|
||||
response.cookies.set('accessToken', accessToken, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
maxAge: 15 * 60, // 15 minutes
|
||||
path: '/',
|
||||
})
|
||||
|
||||
response.cookies.set('refreshToken', newRefreshToken, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
maxAge: 7 * 24 * 60 * 60, // 7 days
|
||||
path: '/',
|
||||
})
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('Token refresh error:', error)
|
||||
|
||||
return NextResponse.json(
|
||||
{ success: false, error: { message: 'Internal server error', code: 'INTERNAL_ERROR' } },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
109
app/api/auth/register/route.ts
Normal file
109
app/api/auth/register/route.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import connectDB from '@/lib/mongodb'
|
||||
import { User, UserSchema } from '@/models/user'
|
||||
import { generateTokens } from '@/lib/jwt'
|
||||
import { generateSiliconId } from '@/lib/siliconId'
|
||||
|
||||
const RegisterSchema = UserSchema.pick({
|
||||
email: true,
|
||||
name: true,
|
||||
password: true,
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
|
||||
// Validate input
|
||||
const validatedData = RegisterSchema.parse(body)
|
||||
|
||||
// Connect to database
|
||||
await connectDB()
|
||||
|
||||
// Check if user already exists
|
||||
const existingUser = await User.findOne({ email: validatedData.email.toLowerCase() })
|
||||
if (existingUser) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: { message: 'User already exists', code: 'USER_EXISTS' } },
|
||||
{ status: 409 }
|
||||
)
|
||||
}
|
||||
|
||||
// Generate tokens first
|
||||
const tempUser = {
|
||||
email: validatedData.email.toLowerCase(),
|
||||
name: validatedData.name,
|
||||
role: 'user' as const,
|
||||
}
|
||||
|
||||
// Generate unique Silicon ID
|
||||
const siliconId = generateSiliconId()
|
||||
console.log('Generated siliconId:', siliconId)
|
||||
|
||||
// Create new user
|
||||
const user = new User({
|
||||
email: tempUser.email,
|
||||
name: tempUser.name,
|
||||
password: validatedData.password,
|
||||
siliconId: siliconId,
|
||||
provider: 'local',
|
||||
})
|
||||
console.log('User before save:', JSON.stringify(user.toObject(), null, 2))
|
||||
await user.save()
|
||||
console.log('user after save:', user)
|
||||
|
||||
// Generate tokens with actual user ID
|
||||
const { accessToken, refreshToken } = generateTokens({
|
||||
userId: user._id.toString(),
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
})
|
||||
|
||||
// Update user with refresh token (single save)
|
||||
user.refreshToken = refreshToken
|
||||
await user.save()
|
||||
|
||||
// Create response with tokens
|
||||
const response = NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
user: user.toJSON(),
|
||||
accessToken,
|
||||
},
|
||||
})
|
||||
|
||||
// Set HTTP-only cookies
|
||||
response.cookies.set('accessToken', accessToken, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
maxAge: 15 * 60, // 15 minutes
|
||||
path: '/',
|
||||
})
|
||||
|
||||
response.cookies.set('refreshToken', refreshToken, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
maxAge: 7 * 24 * 60 * 60, // 7 days
|
||||
path: '/',
|
||||
})
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('Registration error:', error)
|
||||
|
||||
if (error instanceof z.ZodError) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: { message: error.issues[0].message, code: 'VALIDATION_ERROR' } },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{ success: false, error: { message: 'Internal server error', code: 'INTERNAL_ERROR' } },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
237
app/api/balance/add/route.ts
Normal file
237
app/api/balance/add/route.ts
Normal file
@@ -0,0 +1,237 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import jwt from 'jsonwebtoken'
|
||||
import crypto from 'crypto'
|
||||
|
||||
interface AddBalanceRequest {
|
||||
amount: number
|
||||
currency: 'INR' | 'USD'
|
||||
}
|
||||
|
||||
interface UserTokenPayload {
|
||||
siliconId: string
|
||||
email: string
|
||||
type: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Add Balance API
|
||||
* Initiates "Add Balance" transaction for user accounts
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
// Verify user authentication
|
||||
const token = request.cookies.get('accessToken')?.value
|
||||
if (!token) {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: 'Authentication required' },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const secret = process.env.JWT_SECRET || 'your-secret-key'
|
||||
const user = jwt.verify(token, secret) as UserTokenPayload
|
||||
|
||||
// Parse request body
|
||||
const body: AddBalanceRequest = await request.json()
|
||||
const { amount, currency } = body
|
||||
|
||||
// Validate input
|
||||
if (!amount || amount <= 0 || !['INR', 'USD'].includes(currency)) {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: 'Invalid amount or currency' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Validate amount limits
|
||||
const minAmount = currency === 'INR' ? 100 : 2 // Minimum ₹100 or $2
|
||||
const maxAmount = currency === 'INR' ? 100000 : 1500 // Maximum ₹1,00,000 or $1500
|
||||
|
||||
if (amount < minAmount || amount > maxAmount) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
message: `Amount must be between ${currency === 'INR' ? '₹' : '$'}${minAmount} and ${currency === 'INR' ? '₹' : '$'}${maxAmount}`,
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
// Generate unique transaction ID
|
||||
const transactionId = generateTransactionId()
|
||||
|
||||
// TODO: Save transaction to database
|
||||
// In real implementation:
|
||||
// await saveAddBalanceTransaction({
|
||||
// siliconId: user.siliconId,
|
||||
// amount,
|
||||
// currency,
|
||||
// transaction_id: transactionId,
|
||||
// status: 'pending'
|
||||
// })
|
||||
|
||||
// Mock database save for demo
|
||||
await mockSaveAddBalanceTransaction(user.siliconId, amount, currency, transactionId)
|
||||
|
||||
// PayU configuration
|
||||
const merchantKey = process.env.PAYU_MERCHANT_KEY || 'test-key'
|
||||
const merchantSalt = process.env.PAYU_MERCHANT_SALT || 'test-salt'
|
||||
const payuUrl = process.env.PAYU_URL || 'https://test.payu.in/_payment'
|
||||
|
||||
// Prepare payment data
|
||||
const productinfo = 'add_balance'
|
||||
const firstname = 'Customer'
|
||||
const email = user.email
|
||||
const phone = '9876543210' // Default phone or fetch from user profile
|
||||
|
||||
// Success and failure URLs for balance transactions
|
||||
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:4023'
|
||||
const surl = `${baseUrl}/api/balance/success`
|
||||
const furl = `${baseUrl}/api/balance/failure`
|
||||
|
||||
// Generate PayU hash
|
||||
const hashString = `${merchantKey}|${transactionId}|${amount}|${productinfo}|${firstname}|${email}|||||||||||${merchantSalt}`
|
||||
const hash = crypto.createHash('sha512').update(hashString).digest('hex')
|
||||
|
||||
// Return payment form data for frontend submission
|
||||
const paymentData = {
|
||||
success: true,
|
||||
transaction_id: transactionId,
|
||||
currency,
|
||||
payment_url: payuUrl,
|
||||
form_data: {
|
||||
key: merchantKey,
|
||||
txnid: transactionId,
|
||||
amount: amount.toFixed(2),
|
||||
productinfo,
|
||||
firstname,
|
||||
email,
|
||||
phone,
|
||||
surl,
|
||||
furl,
|
||||
hash,
|
||||
service_provider: 'payu_paisa',
|
||||
},
|
||||
}
|
||||
|
||||
return NextResponse.json(paymentData)
|
||||
} catch (dbError) {
|
||||
console.error('Database error during add balance:', dbError)
|
||||
return NextResponse.json(
|
||||
{ success: false, message: 'Failed to initiate balance transaction' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Add balance error:', error)
|
||||
return NextResponse.json(
|
||||
{ success: false, message: 'Add balance initiation failed' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Add Balance History
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
// Verify user authentication
|
||||
const token = request.cookies.get('accessToken')?.value
|
||||
if (!token) {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: 'Authentication required' },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const secret = process.env.JWT_SECRET || 'your-secret-key'
|
||||
const user = jwt.verify(token, secret) as UserTokenPayload
|
||||
|
||||
// TODO: Fetch balance history from database
|
||||
// In real implementation:
|
||||
// const history = await getBalanceHistory(user.siliconId)
|
||||
|
||||
// Mock balance history for demo
|
||||
const mockHistory = await getMockBalanceHistory(user.siliconId)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
balance_history: mockHistory,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Get balance history error:', error)
|
||||
return NextResponse.json(
|
||||
{ success: false, message: 'Failed to fetch balance history' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Utility functions
|
||||
function generateTransactionId(): string {
|
||||
const timestamp = Date.now().toString(36)
|
||||
const random = Math.random().toString(36).substr(2, 5)
|
||||
return `bal_${timestamp}${random}`.toUpperCase()
|
||||
}
|
||||
|
||||
// Mock functions for demonstration
|
||||
async function mockSaveAddBalanceTransaction(
|
||||
siliconId: string,
|
||||
amount: number,
|
||||
currency: string,
|
||||
transactionId: string
|
||||
) {
|
||||
console.log(`Mock DB Save: Add Balance Transaction`)
|
||||
console.log(`SiliconID: ${siliconId}, Amount: ${amount} ${currency}, TxnID: ${transactionId}`)
|
||||
|
||||
// Mock database record
|
||||
const transaction = {
|
||||
id: Date.now(),
|
||||
silicon_id: siliconId,
|
||||
transaction_id: transactionId,
|
||||
amount,
|
||||
currency,
|
||||
status: 'pending',
|
||||
created_at: new Date(),
|
||||
updated_at: new Date(),
|
||||
}
|
||||
|
||||
// Mock delay
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
return transaction
|
||||
}
|
||||
|
||||
async function getMockBalanceHistory(siliconId: string) {
|
||||
// Mock balance transaction history
|
||||
return [
|
||||
{
|
||||
id: 1,
|
||||
transaction_id: 'BAL_123ABC',
|
||||
amount: 1000,
|
||||
currency: 'INR',
|
||||
status: 'success',
|
||||
created_at: '2025-01-15T10:30:00Z',
|
||||
updated_at: '2025-01-15T10:31:00Z',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
transaction_id: 'BAL_456DEF',
|
||||
amount: 500,
|
||||
currency: 'INR',
|
||||
status: 'failed',
|
||||
created_at: '2025-01-10T14:20:00Z',
|
||||
updated_at: '2025-01-10T14:21:00Z',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
transaction_id: 'BAL_789GHI',
|
||||
amount: 2000,
|
||||
currency: 'INR',
|
||||
status: 'success',
|
||||
created_at: '2025-01-05T09:15:00Z',
|
||||
updated_at: '2025-01-05T09:16:00Z',
|
||||
},
|
||||
]
|
||||
}
|
||||
169
app/api/balance/deduct/route.ts
Normal file
169
app/api/balance/deduct/route.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { authMiddleware } from '@/lib/auth-middleware'
|
||||
import connectDB from '@/lib/mongodb'
|
||||
import { User as UserModel } from '@/models/user'
|
||||
import { Transaction } from '@/models/transaction'
|
||||
|
||||
// Schema for balance deduction request
|
||||
const DeductBalanceSchema = z.object({
|
||||
amount: z.number().positive('Amount must be positive'),
|
||||
service: z.string().min(1, 'Service name is required'),
|
||||
serviceId: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
transactionId: z.string().optional(),
|
||||
})
|
||||
|
||||
// Schema for balance deduction response
|
||||
const DeductBalanceResponseSchema = z.object({
|
||||
success: z.boolean(),
|
||||
data: z
|
||||
.object({
|
||||
transactionId: z.string(),
|
||||
previousBalance: z.number(),
|
||||
newBalance: z.number(),
|
||||
amountDeducted: z.number(),
|
||||
service: z.string(),
|
||||
timestamp: z.string(),
|
||||
})
|
||||
.optional(),
|
||||
error: z
|
||||
.object({
|
||||
message: z.string(),
|
||||
code: z.string(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
// Authenticate user
|
||||
const user = await authMiddleware(request)
|
||||
if (!user) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: { message: 'Authentication required', code: 'UNAUTHORIZED' },
|
||||
},
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
await connectDB()
|
||||
|
||||
// Parse and validate request body
|
||||
const body = await request.json()
|
||||
const validatedData = DeductBalanceSchema.parse(body)
|
||||
|
||||
// Get user's current balance from database
|
||||
const userData = await UserModel.findOne({ email: user.email })
|
||||
if (!userData) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: { message: 'User not found', code: 'USER_NOT_FOUND' },
|
||||
},
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
const currentBalance = userData.balance || 0
|
||||
|
||||
// Check if user has sufficient balance
|
||||
if (currentBalance < validatedData.amount) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: `Insufficient balance. Required: ₹${validatedData.amount}, Available: ₹${currentBalance}`,
|
||||
code: 'INSUFFICIENT_BALANCE',
|
||||
},
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Calculate new balance
|
||||
const newBalance = currentBalance - validatedData.amount
|
||||
|
||||
// Update user balance in database
|
||||
await UserModel.updateOne(
|
||||
{ email: user.email },
|
||||
{
|
||||
$set: { balance: newBalance },
|
||||
$inc: { __v: 1 }, // Increment version for optimistic locking
|
||||
}
|
||||
)
|
||||
|
||||
// Generate transaction ID if not provided
|
||||
const transactionId =
|
||||
validatedData.transactionId || `txn_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
||||
|
||||
// Save transaction record to database
|
||||
try {
|
||||
const newTransaction = new Transaction({
|
||||
transactionId,
|
||||
userId: user.id,
|
||||
email: user.email,
|
||||
type: 'debit',
|
||||
amount: validatedData.amount,
|
||||
service: validatedData.service,
|
||||
serviceId: validatedData.serviceId,
|
||||
description: validatedData.description || `Payment for ${validatedData.service}`,
|
||||
status: 'completed',
|
||||
previousBalance: currentBalance,
|
||||
newBalance,
|
||||
metadata: {
|
||||
userAgent: request.headers.get('user-agent'),
|
||||
ip: request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip'),
|
||||
},
|
||||
})
|
||||
|
||||
await newTransaction.save()
|
||||
console.log('Transaction record saved:', transactionId)
|
||||
} catch (transactionError) {
|
||||
console.error('Failed to save transaction record:', transactionError)
|
||||
// Continue even if transaction record fails - balance was already deducted
|
||||
}
|
||||
|
||||
const responseData = {
|
||||
success: true,
|
||||
data: {
|
||||
transactionId,
|
||||
previousBalance: currentBalance,
|
||||
newBalance,
|
||||
amountDeducted: validatedData.amount,
|
||||
service: validatedData.service,
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
}
|
||||
|
||||
// Validate response format
|
||||
const validatedResponse = DeductBalanceResponseSchema.parse(responseData)
|
||||
return NextResponse.json(validatedResponse, { status: 200 })
|
||||
} catch (error) {
|
||||
console.error('Balance deduction error:', error)
|
||||
|
||||
if (error instanceof z.ZodError) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Invalid request data',
|
||||
code: 'VALIDATION_ERROR',
|
||||
details: error.issues,
|
||||
},
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: { message: 'Failed to deduct balance', code: 'INTERNAL_ERROR' },
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
147
app/api/balance/failure/route.ts
Normal file
147
app/api/balance/failure/route.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import crypto from 'crypto'
|
||||
|
||||
interface PayUBalanceFailureResponse {
|
||||
status: string
|
||||
txnid: string
|
||||
amount: string
|
||||
productinfo: string
|
||||
firstname: string
|
||||
email: string
|
||||
hash: string
|
||||
key: string
|
||||
error?: string
|
||||
error_Message?: string
|
||||
[key: string]: string | undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Balance Addition Failure Handler
|
||||
* Processes failed "Add Balance" payments from PayU gateway
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const formData = await request.formData()
|
||||
const payuResponse: Partial<PayUBalanceFailureResponse> = {}
|
||||
|
||||
// Extract all PayU response parameters
|
||||
for (const [key, value] of formData.entries()) {
|
||||
payuResponse[key] = value.toString()
|
||||
}
|
||||
|
||||
const { status, txnid, amount, productinfo, firstname, email, hash, key: merchantKey, error, error_Message } = payuResponse
|
||||
|
||||
// Log failure details for debugging
|
||||
console.log(`Balance addition failed - Transaction: ${txnid}, Status: ${status}, Error: ${error || error_Message || 'Unknown'}`)
|
||||
|
||||
// Validate required parameters
|
||||
if (!txnid) {
|
||||
console.error('Missing transaction ID in balance failure response')
|
||||
return NextResponse.redirect(new URL('/payment/failed?error=invalid-response&type=balance', request.url))
|
||||
}
|
||||
|
||||
// Verify PayU hash if provided (for security)
|
||||
if (hash && merchantKey && status && amount) {
|
||||
const merchantSalt = process.env.PAYU_MERCHANT_SALT || 'test-salt'
|
||||
const expectedHashString = `${merchantSalt}|${status}|||||||||||${email}|${firstname}|${productinfo}|${amount}|${txnid}|${merchantKey}`
|
||||
const expectedHash = crypto.createHash('sha512').update(expectedHashString).digest('hex').toLowerCase()
|
||||
|
||||
if (hash.toLowerCase() !== expectedHash) {
|
||||
console.error(`Hash mismatch in balance failure response for transaction: ${txnid}`)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// TODO: Update database with failed balance transaction
|
||||
// In real implementation:
|
||||
// await updateAddBalanceStatus(txnid, 'failed', error || error_Message)
|
||||
|
||||
// Mock database update for demo
|
||||
await mockUpdateBalanceFailure(txnid as string, error || error_Message || 'Payment failed')
|
||||
|
||||
// Determine failure reason for user-friendly message
|
||||
let failureReason = 'unknown'
|
||||
let userMessage = 'Your balance addition failed. Please try again.'
|
||||
|
||||
if (error_Message?.toLowerCase().includes('insufficient')) {
|
||||
failureReason = 'insufficient-funds'
|
||||
userMessage = 'Insufficient funds in your payment method.'
|
||||
} else if (error_Message?.toLowerCase().includes('declined')) {
|
||||
failureReason = 'card-declined'
|
||||
userMessage = 'Your card was declined. Please try a different payment method.'
|
||||
} else if (error_Message?.toLowerCase().includes('timeout')) {
|
||||
failureReason = 'timeout'
|
||||
userMessage = 'Payment timed out. Please try again.'
|
||||
} else if (error_Message?.toLowerCase().includes('cancelled')) {
|
||||
failureReason = 'user-cancelled'
|
||||
userMessage = 'Payment was cancelled.'
|
||||
} else if (error_Message?.toLowerCase().includes('invalid')) {
|
||||
failureReason = 'invalid-details'
|
||||
userMessage = 'Invalid payment details. Please check and try again.'
|
||||
}
|
||||
|
||||
// Redirect to profile page with failure message
|
||||
const failureUrl = new URL('/profile', request.url)
|
||||
failureUrl.searchParams.set('payment', 'failed')
|
||||
failureUrl.searchParams.set('type', 'balance')
|
||||
failureUrl.searchParams.set('reason', failureReason)
|
||||
failureUrl.searchParams.set('txn', txnid as string)
|
||||
failureUrl.searchParams.set('message', encodeURIComponent(userMessage))
|
||||
if (amount) {
|
||||
failureUrl.searchParams.set('amount', amount as string)
|
||||
}
|
||||
|
||||
return NextResponse.redirect(failureUrl)
|
||||
|
||||
} catch (dbError) {
|
||||
console.error('Database update error during balance failure handling:', dbError)
|
||||
// Continue to failure page even if DB update fails
|
||||
const failureUrl = new URL('/profile', request.url)
|
||||
failureUrl.searchParams.set('payment', 'failed')
|
||||
failureUrl.searchParams.set('type', 'balance')
|
||||
failureUrl.searchParams.set('error', 'db-error')
|
||||
failureUrl.searchParams.set('txn', txnid as string)
|
||||
|
||||
return NextResponse.redirect(failureUrl)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Balance failure handler error:', error)
|
||||
return NextResponse.redirect(new URL('/profile?payment=failed&type=balance&error=processing-error', request.url))
|
||||
}
|
||||
}
|
||||
|
||||
// Mock function for demonstration
|
||||
async function mockUpdateBalanceFailure(txnid: string, errorMessage: string) {
|
||||
console.log(`Mock DB Update: Balance Transaction ${txnid} failed`)
|
||||
console.log(`Failure reason: ${errorMessage}`)
|
||||
|
||||
// Mock: Update add_balance_history status
|
||||
const historyUpdate = {
|
||||
transaction_id: txnid,
|
||||
status: 'failed',
|
||||
error_message: errorMessage,
|
||||
updated_at: new Date(),
|
||||
retry_count: 1, // Could track retry attempts
|
||||
payment_gateway_response: {
|
||||
status: 'failed',
|
||||
error: errorMessage,
|
||||
failed_at: new Date()
|
||||
}
|
||||
}
|
||||
|
||||
// Mock: Could also track failed attempt statistics
|
||||
const analytics = {
|
||||
failed_balance_additions: 1,
|
||||
common_failure_reasons: {
|
||||
[errorMessage]: 1
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Mock: Balance failure recorded for analysis')
|
||||
|
||||
// Mock delay
|
||||
await new Promise(resolve => setTimeout(resolve, 100))
|
||||
|
||||
return { historyUpdate, analytics }
|
||||
}
|
||||
132
app/api/balance/success/route.ts
Normal file
132
app/api/balance/success/route.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import crypto from 'crypto'
|
||||
|
||||
interface PayUBalanceResponse {
|
||||
status: string
|
||||
txnid: string
|
||||
amount: string
|
||||
productinfo: string
|
||||
firstname: string
|
||||
email: string
|
||||
hash: string
|
||||
key: string
|
||||
[key: string]: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Balance Addition Success Handler
|
||||
* Processes successful "Add Balance" payments from PayU gateway
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const formData = await request.formData()
|
||||
const payuResponse: Partial<PayUBalanceResponse> = {}
|
||||
|
||||
// Extract all PayU response parameters
|
||||
for (const [key, value] of formData.entries()) {
|
||||
payuResponse[key] = value.toString()
|
||||
}
|
||||
|
||||
const { status, txnid, amount, productinfo, firstname, email, hash, key: merchantKey } = payuResponse
|
||||
|
||||
// Validate required parameters
|
||||
if (!status || !txnid || !amount || !hash) {
|
||||
console.error('Missing required PayU parameters in balance success')
|
||||
return NextResponse.redirect(new URL('/payment/failed?error=invalid-response', request.url))
|
||||
}
|
||||
|
||||
// Verify payment status
|
||||
if (status !== 'success') {
|
||||
console.log(`Balance payment failed for transaction: ${txnid}, status: ${status}`)
|
||||
return NextResponse.redirect(new URL('/payment/failed?txn=' + txnid + '&type=balance', request.url))
|
||||
}
|
||||
|
||||
// Verify PayU hash for security
|
||||
const merchantSalt = process.env.PAYU_MERCHANT_SALT || 'test-salt'
|
||||
const expectedHashString = `${merchantSalt}|${status}|||||||||||${email}|${firstname}|${productinfo}|${amount}|${txnid}|${merchantKey}`
|
||||
const expectedHash = crypto.createHash('sha512').update(expectedHashString).digest('hex').toLowerCase()
|
||||
|
||||
if (hash?.toLowerCase() !== expectedHash) {
|
||||
console.error(`Hash mismatch for balance transaction: ${txnid}`)
|
||||
return NextResponse.redirect(new URL('/payment/failed?error=hash-mismatch&type=balance', request.url))
|
||||
}
|
||||
|
||||
try {
|
||||
// TODO: Update database with successful balance addition
|
||||
// In real implementation:
|
||||
// const siliconId = await getSiliconIdFromTransaction(txnid)
|
||||
// await updateAddBalanceStatus(txnid, 'success')
|
||||
// await incrementUserBalance(siliconId, parseFloat(amount))
|
||||
|
||||
console.log(`Balance addition successful for transaction: ${txnid}, amount: ₹${amount}`)
|
||||
|
||||
// Mock database update for demo
|
||||
const updateResult = await mockUpdateBalanceSuccess(txnid as string, parseFloat(amount as string))
|
||||
|
||||
// Redirect to profile page with success message
|
||||
const successUrl = new URL('/profile', request.url)
|
||||
successUrl.searchParams.set('payment', 'success')
|
||||
successUrl.searchParams.set('type', 'balance')
|
||||
successUrl.searchParams.set('amount', amount as string)
|
||||
successUrl.searchParams.set('txn', txnid as string)
|
||||
|
||||
return NextResponse.redirect(successUrl)
|
||||
|
||||
} catch (dbError) {
|
||||
console.error('Database update error during balance success:', dbError)
|
||||
// Log for manual reconciliation - payment was successful at gateway
|
||||
console.error(`CRITICAL: Balance payment ${txnid} succeeded at PayU but DB update failed`)
|
||||
|
||||
// Still redirect to success but with warning
|
||||
const successUrl = new URL('/profile', request.url)
|
||||
successUrl.searchParams.set('payment', 'success')
|
||||
successUrl.searchParams.set('type', 'balance')
|
||||
successUrl.searchParams.set('amount', amount as string)
|
||||
successUrl.searchParams.set('txn', txnid as string)
|
||||
successUrl.searchParams.set('warning', 'db-update-failed')
|
||||
|
||||
return NextResponse.redirect(successUrl)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Balance success handler error:', error)
|
||||
return NextResponse.redirect(new URL('/payment/failed?error=processing-error&type=balance', request.url))
|
||||
}
|
||||
}
|
||||
|
||||
// Mock function for demonstration
|
||||
async function mockUpdateBalanceSuccess(txnid: string, amount: number) {
|
||||
console.log(`Mock DB Update: Balance Transaction ${txnid} successful`)
|
||||
|
||||
// Mock: Get user Silicon ID from transaction
|
||||
const mockSiliconId = 'USR_12345' // In real implementation, fetch from add_balance_history table
|
||||
|
||||
// Mock: Update add_balance_history status
|
||||
const historyUpdate = {
|
||||
transaction_id: txnid,
|
||||
status: 'success',
|
||||
updated_at: new Date(),
|
||||
payment_gateway_response: {
|
||||
amount: amount,
|
||||
currency: 'INR',
|
||||
payment_date: new Date()
|
||||
}
|
||||
}
|
||||
|
||||
// Mock: Update user balance
|
||||
const balanceUpdate = {
|
||||
silicon_id: mockSiliconId,
|
||||
balance_increment: amount,
|
||||
previous_balance: 5000, // Mock previous balance
|
||||
new_balance: 5000 + amount,
|
||||
updated_at: new Date()
|
||||
}
|
||||
|
||||
console.log(`Mock: User ${mockSiliconId} balance increased by ₹${amount}`)
|
||||
console.log(`Mock: New balance: ₹${balanceUpdate.new_balance}`)
|
||||
|
||||
// Mock delay
|
||||
await new Promise(resolve => setTimeout(resolve, 150))
|
||||
|
||||
return { historyUpdate, balanceUpdate }
|
||||
}
|
||||
102
app/api/billing/route.ts
Normal file
102
app/api/billing/route.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { authMiddleware } from '@/lib/auth-middleware'
|
||||
import connectDB from '@/lib/mongodb'
|
||||
import { User as UserModel } from '@/models/user'
|
||||
import BillingService from '@/lib/billing-service'
|
||||
|
||||
// Schema for billing query parameters
|
||||
const BillingQuerySchema = z.object({
|
||||
serviceType: z.string().optional(),
|
||||
status: z.string().optional(),
|
||||
limit: z.coerce.number().min(1).max(100).default(20),
|
||||
offset: z.coerce.number().min(0).default(0),
|
||||
})
|
||||
|
||||
// GET endpoint to fetch user's billing records
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const user = await authMiddleware(request)
|
||||
if (!user) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: { message: 'Authentication required', code: 'UNAUTHORIZED' },
|
||||
},
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
await connectDB()
|
||||
|
||||
const { searchParams } = new URL(request.url)
|
||||
const queryParams = {
|
||||
serviceType: searchParams.get('serviceType') || undefined,
|
||||
status: searchParams.get('status') || undefined,
|
||||
limit: searchParams.get('limit') || '20',
|
||||
offset: searchParams.get('offset') || '0',
|
||||
}
|
||||
|
||||
const validatedParams = BillingQuerySchema.parse(queryParams)
|
||||
|
||||
// Get user data
|
||||
const userData = await UserModel.findOne({ email: user.email })
|
||||
if (!userData) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: { message: 'User not found', code: 'USER_NOT_FOUND' },
|
||||
},
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
// Fetch billing records
|
||||
const billings = await BillingService.getUserBillings(user.email, user.id, {
|
||||
serviceType: validatedParams.serviceType,
|
||||
status: validatedParams.status,
|
||||
limit: validatedParams.limit,
|
||||
offset: validatedParams.offset,
|
||||
})
|
||||
|
||||
// Get billing statistics
|
||||
const stats = await BillingService.getBillingStats(user.email, user.id)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
billings,
|
||||
stats,
|
||||
pagination: {
|
||||
limit: validatedParams.limit,
|
||||
offset: validatedParams.offset,
|
||||
total: billings.length,
|
||||
},
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch billing records:', error)
|
||||
|
||||
if (error instanceof z.ZodError) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Invalid query parameters',
|
||||
code: 'VALIDATION_ERROR',
|
||||
details: error.issues,
|
||||
},
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: { message: 'Failed to fetch billing records', code: 'INTERNAL_ERROR' },
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
57
app/api/billing/stats/route.ts
Normal file
57
app/api/billing/stats/route.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { authMiddleware } from '@/lib/auth-middleware'
|
||||
import connectDB from '@/lib/mongodb'
|
||||
import { User as UserModel } from '@/models/user'
|
||||
import BillingService from '@/lib/billing-service'
|
||||
|
||||
// GET endpoint to fetch user's billing statistics
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const user = await authMiddleware(request)
|
||||
if (!user) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: { message: 'Authentication required', code: 'UNAUTHORIZED' },
|
||||
},
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
await connectDB()
|
||||
|
||||
// Get user data
|
||||
const userData = await UserModel.findOne({ email: user.email })
|
||||
if (!userData) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: { message: 'User not found', code: 'USER_NOT_FOUND' },
|
||||
},
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
// Get comprehensive billing statistics
|
||||
const stats = await BillingService.getBillingStats(user.email, user.id)
|
||||
const activeServices = await BillingService.getActiveServices(user.email, user.id)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
...stats,
|
||||
activeServicesList: activeServices,
|
||||
currentBalance: userData.balance || 0,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch billing statistics:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: { message: 'Failed to fetch billing statistics', code: 'INTERNAL_ERROR' },
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
80
app/api/contact/route.ts
Normal file
80
app/api/contact/route.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
|
||||
// Validation schema matching sp_25 structure
|
||||
const contactSchema = z.object({
|
||||
name: z.string().min(1, 'Name is required').trim(),
|
||||
email: z.string().email('Invalid email format').trim(),
|
||||
company: z.string().optional(),
|
||||
service_intrest: z.string().min(1, 'Service interest is required'),
|
||||
message: z.string().min(1, 'Message is required').trim(),
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
|
||||
// Validate the request body
|
||||
const validatedData = contactSchema.parse(body)
|
||||
|
||||
// Get client IP address
|
||||
const ip_address = request.headers.get('x-forwarded-for') ||
|
||||
request.headers.get('x-real-ip') ||
|
||||
'unknown'
|
||||
|
||||
// TODO: Database integration
|
||||
// This would integrate with MongoDB/database in production
|
||||
// For now, just simulate successful submission
|
||||
|
||||
// TODO: Email notification
|
||||
// This would send email notification in production
|
||||
// const emailData = {
|
||||
// to: 'contact@siliconpin.com',
|
||||
// subject: 'New Contact Form Submission',
|
||||
// body: `
|
||||
// Name: ${validatedData.name}
|
||||
// Email: ${validatedData.email}
|
||||
// Company: ${validatedData.company || 'Not provided'}
|
||||
// Service Interest: ${validatedData.service_intrest}
|
||||
// Message: ${validatedData.message}
|
||||
// IP Address: ${ip_address}
|
||||
// `
|
||||
// }
|
||||
|
||||
// Log the submission (for development)
|
||||
console.log('Contact form submission:', {
|
||||
...validatedData,
|
||||
ip_address,
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
|
||||
// Simulate processing delay
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Thank you for your message! We\'ll get back to you soon.'
|
||||
}, { status: 200 })
|
||||
|
||||
} catch (error) {
|
||||
console.error('Contact form error:', error)
|
||||
|
||||
if (error instanceof z.ZodError) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
message: 'Validation failed',
|
||||
errors: error.issues.reduce((acc, err) => {
|
||||
if (err.path.length > 0) {
|
||||
acc[err.path[0] as string] = err.message
|
||||
}
|
||||
return acc
|
||||
}, {} as Record<string, string>)
|
||||
}, { status: 400 })
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
message: 'Internal server error. Please try again later.'
|
||||
}, { status: 500 })
|
||||
}
|
||||
}
|
||||
138
app/api/dashboard/route.ts
Normal file
138
app/api/dashboard/route.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import connectDB from '@/lib/mongodb'
|
||||
import TopicModel from '@/models/topic'
|
||||
import { authMiddleware } from '@/lib/auth-middleware'
|
||||
|
||||
// Response schema for type safety
|
||||
const DashboardStatsSchema = z.object({
|
||||
success: z.boolean(),
|
||||
data: z.object({
|
||||
stats: z.object({
|
||||
totalTopics: z.number(),
|
||||
publishedTopics: z.number(),
|
||||
draftTopics: z.number(),
|
||||
totalViews: z.number(),
|
||||
}),
|
||||
userTopics: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
title: z.string(),
|
||||
slug: z.string(),
|
||||
publishedAt: z.number(),
|
||||
isDraft: z.boolean(),
|
||||
views: z.number().optional(),
|
||||
excerpt: z.string(),
|
||||
tags: z.array(
|
||||
z.object({
|
||||
name: z.string(),
|
||||
})
|
||||
),
|
||||
})
|
||||
),
|
||||
}),
|
||||
})
|
||||
|
||||
export type DashboardResponse = z.infer<typeof DashboardStatsSchema>
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
// Authenticate user
|
||||
const user = await authMiddleware(request)
|
||||
if (!user) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: { message: 'Authentication required', code: 'UNAUTHORIZED' },
|
||||
},
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// Connect to database
|
||||
await connectDB()
|
||||
|
||||
// MongoDB Aggregation Pipeline for user topic statistics
|
||||
const userTopics = await TopicModel.aggregate([
|
||||
{
|
||||
// Match topics by user ownership
|
||||
$match: {
|
||||
authorId: user.id,
|
||||
},
|
||||
},
|
||||
{
|
||||
// Sort by publication date (newest first)
|
||||
$sort: {
|
||||
publishedAt: -1,
|
||||
},
|
||||
},
|
||||
{
|
||||
// Project only the fields we need for the dashboard
|
||||
$project: {
|
||||
id: '$id',
|
||||
title: '$title',
|
||||
slug: '$slug',
|
||||
publishedAt: '$publishedAt',
|
||||
isDraft: '$isDraft',
|
||||
views: { $ifNull: ['$views', 0] }, // Default to 0 if views field doesn't exist
|
||||
excerpt: '$excerpt',
|
||||
tags: {
|
||||
$map: {
|
||||
input: '$tags',
|
||||
as: 'tag',
|
||||
in: {
|
||||
name: '$$tag.name',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
// Calculate statistics from the aggregated data
|
||||
const totalTopics = userTopics.length
|
||||
const publishedTopics = userTopics.filter((topic) => !topic.isDraft).length
|
||||
const draftTopics = userTopics.filter((topic) => topic.isDraft).length
|
||||
const totalViews = userTopics.reduce((sum, topic) => sum + (topic.views || 0), 0)
|
||||
|
||||
// Prepare response data
|
||||
const responseData = {
|
||||
success: true,
|
||||
data: {
|
||||
stats: {
|
||||
totalTopics,
|
||||
publishedTopics,
|
||||
draftTopics,
|
||||
totalViews,
|
||||
},
|
||||
userTopics,
|
||||
},
|
||||
}
|
||||
|
||||
// Validate response format matches frontend expectations
|
||||
const validatedResponse = DashboardStatsSchema.parse(responseData)
|
||||
|
||||
return NextResponse.json(validatedResponse, { status: 200 })
|
||||
} catch (error) {
|
||||
console.error('Dashboard API error:', error)
|
||||
|
||||
if (error instanceof z.ZodError) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: { message: 'Invalid response format', code: 'VALIDATION_ERROR' },
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: { message: 'Failed to fetch dashboard data', code: 'INTERNAL_ERROR' },
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
39
app/api/debug/topics/route.ts
Normal file
39
app/api/debug/topics/route.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import connectDB from '@/lib/mongodb'
|
||||
import TopicModel from '@/models/topic'
|
||||
|
||||
// Debug endpoint to see what topics exist in the database
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
await connectDB()
|
||||
|
||||
// Get all topics with basic info
|
||||
const topics = await TopicModel.find({}, {
|
||||
id: 1,
|
||||
slug: 1,
|
||||
title: 1,
|
||||
isDraft: 1,
|
||||
views: 1,
|
||||
authorId: 1
|
||||
}).sort({ publishedAt: -1 })
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
count: topics.length,
|
||||
topics: topics.map(topic => ({
|
||||
id: topic.id,
|
||||
slug: topic.slug,
|
||||
title: topic.title,
|
||||
isDraft: topic.isDraft,
|
||||
views: topic.views || 0,
|
||||
authorId: topic.authorId
|
||||
}))
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Debug topics error:', error)
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: error.message
|
||||
}, { status: 500 })
|
||||
}
|
||||
}
|
||||
95
app/api/feedback/route.ts
Normal file
95
app/api/feedback/route.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
|
||||
// Validation schema matching sp_25 structure
|
||||
const feedbackSchema = z.object({
|
||||
type: z.enum(['suggestion', 'report']),
|
||||
name: z.string().min(1, 'Please enter your name.').trim(),
|
||||
email: z.string().email('Please enter a valid email address.').trim(),
|
||||
title: z.string().min(1, 'Title is required').trim(),
|
||||
details: z.string().min(1, 'Details are required').trim(),
|
||||
category: z.string().default('general'),
|
||||
urgency: z.string().default('low'),
|
||||
siliconId: z.string().nullable().optional()
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
|
||||
// Validate the request body
|
||||
const validatedData = feedbackSchema.parse(body)
|
||||
|
||||
// Get client IP address and referrer
|
||||
const ip_address = request.headers.get('x-forwarded-for') ||
|
||||
request.headers.get('x-real-ip') ||
|
||||
'unknown'
|
||||
const referrer = request.headers.get('referer') || 'Direct Access'
|
||||
|
||||
// TODO: Database integration
|
||||
// This would integrate with MongoDB/database in production
|
||||
// INSERT INTO sp_feedback (type, name, email, title, details, urgency, category, referrer, siliconId, ip_address, created_at)
|
||||
|
||||
// TODO: Email notification
|
||||
const emailData = {
|
||||
to: 'contact@siliconpin.com',
|
||||
subject: `New ${validatedData.type.charAt(0).toUpperCase() + validatedData.type.slice(1)} Submission`,
|
||||
body: `
|
||||
Type: ${validatedData.type.charAt(0).toUpperCase() + validatedData.type.slice(1)}
|
||||
Name: ${validatedData.name}
|
||||
Email: ${validatedData.email}
|
||||
Title: ${validatedData.title}
|
||||
Category: ${validatedData.category}
|
||||
${validatedData.type === 'report' ? `Urgency: ${validatedData.urgency}\n` : ''}
|
||||
Details:
|
||||
${validatedData.details}
|
||||
|
||||
Referrer: ${referrer}
|
||||
IP Address: ${ip_address}
|
||||
${validatedData.siliconId ? `SiliconID: ${validatedData.siliconId}\n` : ''}
|
||||
`.trim()
|
||||
}
|
||||
|
||||
// Log the submission (for development)
|
||||
console.log('Feedback submission:', {
|
||||
...validatedData,
|
||||
referrer,
|
||||
ip_address,
|
||||
timestamp: new Date().toISOString(),
|
||||
emailData
|
||||
})
|
||||
|
||||
// Simulate processing delay
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
|
||||
const successMessage = validatedData.type === 'suggestion'
|
||||
? 'Thank you for your suggestion! We appreciate your feedback.'
|
||||
: 'Your report has been submitted. We will look into it as soon as possible.'
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: successMessage
|
||||
}, { status: 200 })
|
||||
|
||||
} catch (error) {
|
||||
console.error('Feedback submission error:', error)
|
||||
|
||||
if (error instanceof z.ZodError) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
message: 'Validation failed',
|
||||
errors: error.issues.reduce((acc, err) => {
|
||||
if (err.path.length > 0) {
|
||||
acc[err.path[0] as string] = err.message
|
||||
}
|
||||
return acc
|
||||
}, {} as Record<string, string>)
|
||||
}, { status: 400 })
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
message: 'Internal server error. Please try again later.'
|
||||
}, { status: 500 })
|
||||
}
|
||||
}
|
||||
63
app/api/health/route.ts
Normal file
63
app/api/health/route.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { connectDB } from '@/lib/mongodb'
|
||||
import { createClient } from 'redis'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const startTime = Date.now()
|
||||
|
||||
const healthCheck = {
|
||||
status: 'healthy',
|
||||
timestamp: new Date().toISOString(),
|
||||
uptime: process.uptime(),
|
||||
environment: process.env.NODE_ENV,
|
||||
version: process.env.npm_package_version || '1.0.0',
|
||||
checks: {
|
||||
database: { status: 'unknown' as 'healthy' | 'unhealthy' | 'unknown' },
|
||||
redis: { status: 'unknown' as 'healthy' | 'unhealthy' | 'unknown' },
|
||||
},
|
||||
responseTime: 0,
|
||||
}
|
||||
|
||||
// Check database connection
|
||||
try {
|
||||
await connectDB()
|
||||
healthCheck.checks.database.status = 'healthy'
|
||||
} catch (error) {
|
||||
healthCheck.checks.database.status = 'unhealthy'
|
||||
healthCheck.status = 'unhealthy'
|
||||
}
|
||||
|
||||
// Check Redis connection
|
||||
try {
|
||||
const redis = createClient({
|
||||
url: process.env.REDIS_URL || 'redis://localhost:6379',
|
||||
})
|
||||
|
||||
await redis.connect()
|
||||
await redis.ping()
|
||||
await redis.disconnect()
|
||||
|
||||
healthCheck.checks.redis.status = 'healthy'
|
||||
} catch (error) {
|
||||
healthCheck.checks.redis.status = 'unhealthy'
|
||||
healthCheck.status = 'unhealthy'
|
||||
}
|
||||
|
||||
// Calculate response time
|
||||
healthCheck.responseTime = Date.now() - startTime
|
||||
|
||||
// Return appropriate status code
|
||||
const statusCode = healthCheck.status === 'healthy' ? 200 : 503
|
||||
|
||||
return NextResponse.json(healthCheck, { status: statusCode })
|
||||
}
|
||||
|
||||
// Readiness check - simpler check for container orchestration
|
||||
export async function HEAD(request: NextRequest) {
|
||||
try {
|
||||
// Quick check if the application is ready to serve requests
|
||||
return new NextResponse(null, { status: 200 })
|
||||
} catch (error) {
|
||||
return new NextResponse(null, { status: 503 })
|
||||
}
|
||||
}
|
||||
115
app/api/payments/failure/route.ts
Normal file
115
app/api/payments/failure/route.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import crypto from 'crypto'
|
||||
|
||||
interface PayUFailureResponse {
|
||||
status: string
|
||||
txnid: string
|
||||
amount: string
|
||||
productinfo: string
|
||||
firstname: string
|
||||
email: string
|
||||
hash: string
|
||||
key: string
|
||||
error?: string
|
||||
error_Message?: string
|
||||
[key: string]: string | undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* PayU Payment Failure Handler
|
||||
* Processes failed payment responses from PayU gateway
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const formData = await request.formData()
|
||||
const payuResponse: Partial<PayUFailureResponse> = {}
|
||||
|
||||
// Extract all PayU response parameters
|
||||
for (const [key, value] of formData.entries()) {
|
||||
payuResponse[key] = value.toString()
|
||||
}
|
||||
|
||||
const { status, txnid, amount, productinfo, firstname, email, hash, key: merchantKey, error, error_Message } = payuResponse
|
||||
|
||||
// Log failure details for debugging
|
||||
console.log(`Payment failed - Transaction: ${txnid}, Status: ${status}, Error: ${error || error_Message || 'Unknown'}`)
|
||||
|
||||
// Validate required parameters
|
||||
if (!txnid) {
|
||||
console.error('Missing transaction ID in failure response')
|
||||
return NextResponse.redirect(new URL('/payment/failed?error=invalid-response', request.url))
|
||||
}
|
||||
|
||||
// Verify PayU hash if provided (for security)
|
||||
if (hash && merchantKey && status && amount) {
|
||||
const merchantSalt = process.env.PAYU_MERCHANT_SALT || 'test-salt'
|
||||
const expectedHashString = `${merchantSalt}|${status}|||||||||||${email}|${firstname}|${productinfo}|${amount}|${txnid}|${merchantKey}`
|
||||
const expectedHash = crypto.createHash('sha512').update(expectedHashString).digest('hex').toLowerCase()
|
||||
|
||||
if (hash.toLowerCase() !== expectedHash) {
|
||||
console.error(`Hash mismatch in failure response for transaction: ${txnid}`)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// TODO: Update database with failed payment status
|
||||
// In a real implementation:
|
||||
// await updateBillingStatus(txnid, 'failed', error || error_Message)
|
||||
|
||||
// Mock database update for demo
|
||||
await mockUpdatePaymentStatus(txnid as string, 'failed', error || error_Message || 'Payment failed')
|
||||
|
||||
// Determine failure reason for user-friendly message
|
||||
let failureReason = 'unknown'
|
||||
if (error_Message?.toLowerCase().includes('insufficient')) {
|
||||
failureReason = 'insufficient-funds'
|
||||
} else if (error_Message?.toLowerCase().includes('declined')) {
|
||||
failureReason = 'card-declined'
|
||||
} else if (error_Message?.toLowerCase().includes('timeout')) {
|
||||
failureReason = 'timeout'
|
||||
} else if (error_Message?.toLowerCase().includes('cancelled')) {
|
||||
failureReason = 'user-cancelled'
|
||||
}
|
||||
|
||||
// Redirect to failure page with transaction details
|
||||
const failureUrl = new URL('/payment/failed', request.url)
|
||||
failureUrl.searchParams.set('txn', txnid as string)
|
||||
failureUrl.searchParams.set('reason', failureReason)
|
||||
if (amount) {
|
||||
failureUrl.searchParams.set('amount', amount as string)
|
||||
}
|
||||
|
||||
return NextResponse.redirect(failureUrl)
|
||||
|
||||
} catch (dbError) {
|
||||
console.error('Database update error during failure handling:', dbError)
|
||||
// Continue to failure page even if DB update fails
|
||||
return NextResponse.redirect(new URL('/payment/failed?error=db-error&txn=' + txnid, request.url))
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Payment failure handler error:', error)
|
||||
return NextResponse.redirect(new URL('/payment/failed?error=processing-error', request.url))
|
||||
}
|
||||
}
|
||||
|
||||
// Mock function for demonstration
|
||||
async function mockUpdatePaymentStatus(txnid: string, status: string, errorMessage: string) {
|
||||
// In real implementation, this would update MongoDB/database
|
||||
console.log(`Mock DB Update: Transaction ${txnid} marked as ${status}`)
|
||||
console.log(`Failure reason: ${errorMessage}`)
|
||||
|
||||
// Simulate database operations
|
||||
const billingUpdate = {
|
||||
billing_id: txnid,
|
||||
payment_status: status,
|
||||
payment_date: new Date(),
|
||||
error_message: errorMessage,
|
||||
retry_count: 1 // Could track retry attempts
|
||||
}
|
||||
|
||||
// Mock delay
|
||||
await new Promise(resolve => setTimeout(resolve, 100))
|
||||
|
||||
return billingUpdate
|
||||
}
|
||||
112
app/api/payments/initiate/route.ts
Normal file
112
app/api/payments/initiate/route.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import jwt from 'jsonwebtoken'
|
||||
import crypto from 'crypto'
|
||||
|
||||
interface PaymentInitiateRequest {
|
||||
billing_id: string
|
||||
amount: number
|
||||
service: string
|
||||
}
|
||||
|
||||
interface UserTokenPayload {
|
||||
siliconId: string
|
||||
email: string
|
||||
type: string
|
||||
}
|
||||
|
||||
/**
|
||||
* PayU Payment Gateway Integration
|
||||
* Initiates payment for billing records
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
// Verify user authentication
|
||||
const token = request.cookies.get('accessToken')?.value
|
||||
if (!token) {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: 'Authentication required' },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const secret = process.env.JWT_SECRET || 'your-secret-key'
|
||||
const user = jwt.verify(token, secret) as UserTokenPayload
|
||||
|
||||
// Parse request body
|
||||
const body: PaymentInitiateRequest = await request.json()
|
||||
const { billing_id, amount, service } = body
|
||||
|
||||
// Validate input
|
||||
if (!billing_id || !amount || amount <= 0 || !service) {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: 'Invalid payment parameters' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: Verify billing record exists and belongs to user
|
||||
// In a real implementation, you would check database:
|
||||
// const billing = await verifyBillingRecord(billing_id, user.siliconId)
|
||||
|
||||
// PayU configuration (from environment variables)
|
||||
const merchantKey = process.env.PAYU_MERCHANT_KEY || 'test-key'
|
||||
const merchantSalt = process.env.PAYU_MERCHANT_SALT || 'test-salt'
|
||||
const payuUrl = process.env.PAYU_URL || 'https://test.payu.in/_payment'
|
||||
|
||||
// Prepare payment data
|
||||
const txnid = billing_id
|
||||
const productinfo = service.substring(0, 100)
|
||||
const firstname = 'Customer'
|
||||
const email = user.email
|
||||
const phone = '9876543210' // Default phone or fetch from user profile
|
||||
|
||||
// Success and failure URLs
|
||||
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:4023'
|
||||
const surl = `${baseUrl}/api/payments/success`
|
||||
const furl = `${baseUrl}/api/payments/failure`
|
||||
|
||||
// Generate PayU hash
|
||||
const hashString = `${merchantKey}|${txnid}|${amount}|${productinfo}|${firstname}|${email}|||||||||||${merchantSalt}`
|
||||
const hash = crypto.createHash('sha512').update(hashString).digest('hex')
|
||||
|
||||
// Return payment form data for frontend submission
|
||||
const paymentData = {
|
||||
success: true,
|
||||
payment_url: payuUrl,
|
||||
form_data: {
|
||||
key: merchantKey,
|
||||
txnid,
|
||||
amount: amount.toFixed(2),
|
||||
productinfo,
|
||||
firstname,
|
||||
email,
|
||||
phone,
|
||||
surl,
|
||||
furl,
|
||||
hash,
|
||||
service_provider: 'payu_paisa',
|
||||
},
|
||||
}
|
||||
|
||||
return NextResponse.json(paymentData)
|
||||
} catch (error) {
|
||||
console.error('Payment initiation error:', error)
|
||||
return NextResponse.json(
|
||||
{ success: false, message: 'Payment initiation failed' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Mock function - in real implementation, verify against database
|
||||
async function verifyBillingRecord(billingId: string, siliconId: string) {
|
||||
// TODO: Implement database verification
|
||||
// Check if billing record exists and belongs to the user
|
||||
return {
|
||||
billing_id: billingId,
|
||||
amount: 1000,
|
||||
service: 'Cloud Instance',
|
||||
user_silicon_id: siliconId,
|
||||
status: 'pending',
|
||||
}
|
||||
}
|
||||
110
app/api/payments/success/route.ts
Normal file
110
app/api/payments/success/route.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import crypto from 'crypto'
|
||||
|
||||
interface PayUResponse {
|
||||
status: string
|
||||
txnid: string
|
||||
amount: string
|
||||
productinfo: string
|
||||
firstname: string
|
||||
email: string
|
||||
hash: string
|
||||
key: string
|
||||
[key: string]: string
|
||||
}
|
||||
|
||||
/**
|
||||
* PayU Payment Success Handler
|
||||
* Processes successful payment responses from PayU gateway
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const formData = await request.formData()
|
||||
const payuResponse: Partial<PayUResponse> = {}
|
||||
|
||||
// Extract all PayU response parameters
|
||||
for (const [key, value] of formData.entries()) {
|
||||
payuResponse[key] = value.toString()
|
||||
}
|
||||
|
||||
const { status, txnid, amount, productinfo, firstname, email, hash, key: merchantKey } = payuResponse
|
||||
|
||||
// Validate required parameters
|
||||
if (!status || !txnid || !amount || !hash) {
|
||||
console.error('Missing required PayU parameters')
|
||||
return NextResponse.redirect(new URL('/payment/failed?error=invalid-response', request.url))
|
||||
}
|
||||
|
||||
// Verify payment status
|
||||
if (status !== 'success') {
|
||||
console.log(`Payment failed for transaction: ${txnid}, status: ${status}`)
|
||||
return NextResponse.redirect(new URL('/payment/failed?txn=' + txnid, request.url))
|
||||
}
|
||||
|
||||
// Verify PayU hash for security
|
||||
const merchantSalt = process.env.PAYU_MERCHANT_SALT || 'test-salt'
|
||||
const expectedHashString = `${merchantSalt}|${status}|||||||||||${email}|${firstname}|${productinfo}|${amount}|${txnid}|${merchantKey}`
|
||||
const expectedHash = crypto.createHash('sha512').update(expectedHashString).digest('hex').toLowerCase()
|
||||
|
||||
if (hash?.toLowerCase() !== expectedHash) {
|
||||
console.error(`Hash mismatch for transaction: ${txnid}`)
|
||||
// Log potential fraud attempt
|
||||
console.error('Expected hash:', expectedHash)
|
||||
console.error('Received hash:', hash?.toLowerCase())
|
||||
return NextResponse.redirect(new URL('/payment/failed?error=hash-mismatch', request.url))
|
||||
}
|
||||
|
||||
try {
|
||||
// TODO: Update database with successful payment
|
||||
// In a real implementation:
|
||||
// await updateBillingStatus(txnid, 'success')
|
||||
// await updateUserBalance(userSiliconId, parseFloat(amount))
|
||||
|
||||
console.log(`Payment successful for transaction: ${txnid}, amount: ₹${amount}`)
|
||||
|
||||
// Mock database update for demo
|
||||
await mockUpdatePaymentStatus(txnid as string, 'success', parseFloat(amount as string))
|
||||
|
||||
// Redirect to success page with transaction details
|
||||
const successUrl = new URL('/payment/success', request.url)
|
||||
successUrl.searchParams.set('txn', txnid as string)
|
||||
successUrl.searchParams.set('amount', amount as string)
|
||||
|
||||
return NextResponse.redirect(successUrl)
|
||||
|
||||
} catch (dbError) {
|
||||
console.error('Database update error:', dbError)
|
||||
// Even if DB update fails, payment was successful at gateway
|
||||
// Log for manual reconciliation
|
||||
return NextResponse.redirect(new URL('/payment/success?warning=db-update-failed&txn=' + txnid, request.url))
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Payment success handler error:', error)
|
||||
return NextResponse.redirect(new URL('/payment/failed?error=processing-error', request.url))
|
||||
}
|
||||
}
|
||||
|
||||
// Mock function for demonstration
|
||||
async function mockUpdatePaymentStatus(txnid: string, status: string, amount: number) {
|
||||
// In real implementation, this would update MongoDB/database
|
||||
console.log(`Mock DB Update: Transaction ${txnid} marked as ${status}, amount: ₹${amount}`)
|
||||
|
||||
// Simulate database operations
|
||||
const billingUpdate = {
|
||||
billing_id: txnid,
|
||||
payment_status: status,
|
||||
payment_date: new Date(),
|
||||
amount: amount
|
||||
}
|
||||
|
||||
const userBalanceUpdate = {
|
||||
// This would update user's account balance for "add_balance" transactions
|
||||
increment: status === 'success' && txnid.includes('balance') ? amount : 0
|
||||
}
|
||||
|
||||
// Mock delay
|
||||
await new Promise(resolve => setTimeout(resolve, 100))
|
||||
|
||||
return { billingUpdate, userBalanceUpdate }
|
||||
}
|
||||
198
app/api/services/deploy-cloude/route.ts
Normal file
198
app/api/services/deploy-cloude/route.ts
Normal file
@@ -0,0 +1,198 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { authMiddleware } from '@/lib/auth-middleware'
|
||||
import connectDB from '@/lib/mongodb'
|
||||
import { User as UserModel } from '@/models/user'
|
||||
import BillingService from '@/lib/billing-service'
|
||||
import { checkServiceAvailability } from '@/lib/system-settings'
|
||||
|
||||
// VPC subnet mapping based on datacenter
|
||||
const VPC_SUBNET_MAP: { [key: string]: string } = {
|
||||
inmumbaizone2: '3889f7ca-ca19-4851-abc7-5c6b4798b3fe', // Mumbai public subnet
|
||||
inbangalore: 'c17032c9-3cfd-4028-8f2a-f3f5aa8c2976', // Bangalore public subnet
|
||||
innoida: '95c60b59-4925-4b7e-bd05-afbf940d8000', // Delhi/Noida public subnet
|
||||
defra1: '72ea201d-e1e7-4d30-a186-bc709efafad8', // Frankfurt public subnet
|
||||
uslosangeles: '0e253cd1-8ecc-4b65-ae0f-6acbc53b0fb6', // Los Angeles public subnet
|
||||
inbangalore3: 'f687a58b-04f0-4ebe-b583-65788a1d18bf', // Bangalore DC3 public subnet
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
// Check if VPS deployment service is enabled
|
||||
if (!(await checkServiceAvailability('vps'))) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
status: 'error',
|
||||
message: 'VPS deployment service is currently disabled by administrator',
|
||||
},
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
|
||||
// Check authentication using your auth middleware
|
||||
const user = await authMiddleware(request)
|
||||
if (!user) {
|
||||
return NextResponse.json(
|
||||
{ status: 'error', message: 'Unauthorized: Please login to continue' },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
// Get input data from request
|
||||
const input = await request.json()
|
||||
if (!input) {
|
||||
return NextResponse.json({ status: 'error', message: 'Invalid JSON' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Get API key from environment
|
||||
const UTHO_API_KEY = process.env.UTHO_API_KEY
|
||||
if (!UTHO_API_KEY) {
|
||||
return NextResponse.json(
|
||||
{ status: 'error', message: 'API configuration missing' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
// Connect to MongoDB
|
||||
await connectDB()
|
||||
|
||||
// Get user data for billing
|
||||
const userData = await UserModel.findOne({ email: user.email })
|
||||
if (!userData) {
|
||||
return NextResponse.json({ status: 'error', message: 'User not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
const requiredAmount = input.amount || 0
|
||||
|
||||
// Check balance only if amount > 0 (some services might be free)
|
||||
if (requiredAmount > 0) {
|
||||
const currentBalance = userData.balance || 0
|
||||
if (currentBalance < requiredAmount) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
status: 'error',
|
||||
message: `Insufficient balance. Required: ₹${requiredAmount}, Available: ₹${currentBalance}`,
|
||||
code: 'INSUFFICIENT_BALANCE',
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Get VPC subnet based on datacenter
|
||||
const vpcSubnet = VPC_SUBNET_MAP[input.dclocation] || VPC_SUBNET_MAP['inbangalore']
|
||||
|
||||
// Prepare Utho payload - use the exact format from your PHP code
|
||||
const uthoPayload = {
|
||||
dcslug: input.dclocation,
|
||||
planid: input.planid,
|
||||
billingcycle: 'hourly',
|
||||
auth: 'option2',
|
||||
enable_publicip: input.publicip !== false,
|
||||
subnetRequired: false,
|
||||
firewall: '23434645',
|
||||
cpumodel: 'intel',
|
||||
enablebackup: input.backup || false,
|
||||
root_password: input.password,
|
||||
support: 'unmanaged',
|
||||
vpc: vpcSubnet,
|
||||
cloud: [
|
||||
{
|
||||
hostname: input.hostname,
|
||||
},
|
||||
],
|
||||
image: input.image,
|
||||
sshkeys: '',
|
||||
}
|
||||
|
||||
console.log('Sending to Utho API:', uthoPayload)
|
||||
|
||||
// Make API request to Utho
|
||||
const UTHO_API_URL = 'https://api.utho.com/v2/cloud/deploy'
|
||||
const response = await fetch(UTHO_API_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${UTHO_API_KEY}`,
|
||||
'Content-Type': 'application/json',
|
||||
Accept: '*/*',
|
||||
},
|
||||
body: JSON.stringify(uthoPayload),
|
||||
})
|
||||
|
||||
const httpCode = response.status
|
||||
const deploymentSuccess = httpCode >= 200 && httpCode < 300
|
||||
const responseData = await response.json()
|
||||
|
||||
// Add status field like PHP does
|
||||
responseData.status = deploymentSuccess ? 'success' : 'error'
|
||||
|
||||
console.log('Utho API response:', { httpCode, responseData })
|
||||
|
||||
// Process billing using the new comprehensive billing service
|
||||
try {
|
||||
const billingResult = await BillingService.processServiceDeployment({
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
siliconId: userData.siliconId,
|
||||
},
|
||||
service: {
|
||||
name: 'VPS Server',
|
||||
type: 'vps',
|
||||
id: responseData.server_id || responseData.id,
|
||||
instanceId: responseData.server_id || responseData.id,
|
||||
config: {
|
||||
hostname: input.hostname,
|
||||
planid: input.planid,
|
||||
dclocation: input.dclocation,
|
||||
image: input.image,
|
||||
backup: input.backup,
|
||||
publicip: input.publicip,
|
||||
},
|
||||
},
|
||||
amount: requiredAmount,
|
||||
currency: 'INR',
|
||||
cycle: (input.cycle as any) || 'onetime',
|
||||
deploymentSuccess,
|
||||
deploymentResponse: responseData,
|
||||
metadata: {
|
||||
userAgent: request.headers.get('user-agent'),
|
||||
ip: request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip'),
|
||||
uthoPayload,
|
||||
httpCode,
|
||||
},
|
||||
})
|
||||
|
||||
console.log('Billing processed:', {
|
||||
billingId: billingResult.billing.billing_id,
|
||||
transactionId: billingResult.transaction?.transactionId,
|
||||
balanceUpdated: billingResult.balanceUpdated,
|
||||
})
|
||||
} catch (billingError) {
|
||||
console.error('Billing processing failed:', billingError)
|
||||
// Continue even if billing fails, but return error if it's balance-related
|
||||
if (billingError instanceof Error && billingError.message.includes('Insufficient balance')) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
status: 'error',
|
||||
message: billingError.message,
|
||||
code: 'INSUFFICIENT_BALANCE',
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Return the exact same response format as PHP
|
||||
return NextResponse.json(responseData, { status: httpCode })
|
||||
} catch (error) {
|
||||
console.error('Cloud deployment error:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
status: 'error',
|
||||
message: 'Internal server error',
|
||||
details: error instanceof Error ? error.message : 'Unknown error',
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
247
app/api/services/deploy-kubernetes/route.ts
Normal file
247
app/api/services/deploy-kubernetes/route.ts
Normal file
@@ -0,0 +1,247 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { authMiddleware } from '@/lib/auth-middleware'
|
||||
import connectDB from '@/lib/mongodb'
|
||||
import { User as UserModel } from '@/models/user'
|
||||
import BillingService from '@/lib/billing-service'
|
||||
|
||||
// Hardcoded VPC as requested
|
||||
const K8S_VPC = '81b2bd94-61dc-424b-a1ca-ca4c810ed4c4'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
// Check authentication
|
||||
const user = await authMiddleware(request)
|
||||
if (!user) {
|
||||
return NextResponse.json(
|
||||
{ status: 'error', message: 'Unauthorized: Please login to continue' },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
// Get input data from request
|
||||
const input = await request.json()
|
||||
if (!input) {
|
||||
return NextResponse.json({ status: 'error', message: 'Invalid JSON' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if (!input.cluster_label || !input.nodepools) {
|
||||
return NextResponse.json(
|
||||
{ status: 'error', message: 'Cluster label and nodepools are required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Get API key from environment
|
||||
const UTHO_API_KEY = process.env.UTHO_API_KEY
|
||||
if (!UTHO_API_KEY) {
|
||||
return NextResponse.json(
|
||||
{ status: 'error', message: 'API configuration missing' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
// Connect to MongoDB
|
||||
await connectDB()
|
||||
|
||||
// Get user data for billing
|
||||
const userData = await UserModel.findOne({ email: user.email })
|
||||
if (!userData) {
|
||||
return NextResponse.json({ status: 'error', message: 'User not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
const requiredAmount = input.amount || 0
|
||||
|
||||
// Check balance only if amount > 0
|
||||
if (requiredAmount > 0) {
|
||||
const currentBalance = userData.balance || 0
|
||||
if (currentBalance < requiredAmount) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
status: 'error',
|
||||
message: `Insufficient balance. Required: ₹${requiredAmount}, Available: ₹${currentBalance}`,
|
||||
code: 'INSUFFICIENT_BALANCE',
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare Utho payload for Kubernetes
|
||||
const uthoPayload = {
|
||||
dcslug: 'inmumbaizone2', // Hardcoded as requested
|
||||
cluster_label: input.cluster_label,
|
||||
cluster_version: input.cluster_version || '1.30.0-utho',
|
||||
nodepools: input.nodepools,
|
||||
vpc: K8S_VPC, // Hardcoded VPC
|
||||
network_type: 'publicprivate',
|
||||
cpumodel: 'amd',
|
||||
}
|
||||
|
||||
console.log('Sending to Utho Kubernetes API:', uthoPayload)
|
||||
|
||||
// Make API request to Utho Kubernetes
|
||||
const UTHO_API_URL = 'https://api.utho.com/v2/kubernetes/deploy'
|
||||
const response = await fetch(UTHO_API_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${UTHO_API_KEY}`,
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
body: JSON.stringify(uthoPayload),
|
||||
})
|
||||
|
||||
const httpCode = response.status
|
||||
const deploymentSuccess = httpCode >= 200 && httpCode < 300
|
||||
const responseData = await response.json()
|
||||
|
||||
// Add status field and clusterId for consistency
|
||||
responseData.status = deploymentSuccess ? 'success' : 'error'
|
||||
if (deploymentSuccess && responseData.id) {
|
||||
responseData.clusterId = responseData.id
|
||||
}
|
||||
|
||||
console.log('Utho Kubernetes API response:', { httpCode, responseData })
|
||||
|
||||
// Process billing using the new comprehensive billing service
|
||||
try {
|
||||
const billingResult = await BillingService.processServiceDeployment({
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
siliconId: userData.siliconId,
|
||||
},
|
||||
service: {
|
||||
name: `Kubernetes Cluster - ${input.cluster_label}`,
|
||||
type: 'kubernetes',
|
||||
id: responseData.clusterId || responseData.id,
|
||||
clusterId: responseData.clusterId || responseData.id,
|
||||
config: {
|
||||
cluster_label: input.cluster_label,
|
||||
cluster_version: input.cluster_version,
|
||||
nodepools: input.nodepools,
|
||||
dcslug: 'inmumbaizone2',
|
||||
vpc: K8S_VPC,
|
||||
network_type: 'publicprivate',
|
||||
cpumodel: 'amd',
|
||||
},
|
||||
},
|
||||
amount: requiredAmount,
|
||||
currency: 'INR',
|
||||
cycle: (input.cycle as any) || 'onetime',
|
||||
deploymentSuccess,
|
||||
deploymentResponse: responseData,
|
||||
metadata: {
|
||||
userAgent: request.headers.get('user-agent'),
|
||||
ip: request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip'),
|
||||
uthoPayload,
|
||||
httpCode,
|
||||
},
|
||||
})
|
||||
|
||||
console.log('Billing processed:', {
|
||||
billingId: billingResult.billing.billing_id,
|
||||
transactionId: billingResult.transaction?.transactionId,
|
||||
balanceUpdated: billingResult.balanceUpdated,
|
||||
})
|
||||
} catch (billingError) {
|
||||
console.error('Billing processing failed:', billingError)
|
||||
// Continue even if billing fails, but return error if it's balance-related
|
||||
if (billingError instanceof Error && billingError.message.includes('Insufficient balance')) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
status: 'error',
|
||||
message: billingError.message,
|
||||
code: 'INSUFFICIENT_BALANCE',
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json(responseData, { status: httpCode })
|
||||
} catch (error) {
|
||||
console.error('Kubernetes deployment error:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
status: 'error',
|
||||
message: 'Internal server error',
|
||||
details: error instanceof Error ? error.message : 'Unknown error',
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// GET endpoint for downloading kubeconfig
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
// Check authentication
|
||||
const user = await authMiddleware(request)
|
||||
if (!user) {
|
||||
return NextResponse.json(
|
||||
{ status: 'error', message: 'Unauthorized: Please login to continue' },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url)
|
||||
const clusterId = searchParams.get('clusterId')
|
||||
|
||||
if (!clusterId) {
|
||||
return NextResponse.json(
|
||||
{ status: 'error', message: 'Cluster ID is required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Get API key from environment
|
||||
const UTHO_API_KEY = process.env.UTHO_API_KEY
|
||||
if (!UTHO_API_KEY) {
|
||||
return NextResponse.json(
|
||||
{ status: 'error', message: 'API configuration missing' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
// Download kubeconfig
|
||||
const KUBECONFIG_URL = `https://api.utho.com/v2/kubernetes/${clusterId}/download`
|
||||
const response = await fetch(KUBECONFIG_URL, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${UTHO_API_KEY}`,
|
||||
Accept: 'application/yaml',
|
||||
},
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
const kubeconfig = await response.text()
|
||||
|
||||
// Return as downloadable file
|
||||
return new NextResponse(kubeconfig, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/yaml',
|
||||
'Content-Disposition': `attachment; filename="kubeconfig-${clusterId}.yaml"`,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
const errorText = await response.text()
|
||||
console.error('Kubeconfig download failed:', response.status, errorText)
|
||||
return NextResponse.json(
|
||||
{ status: 'error', message: 'Failed to download kubeconfig' },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Kubeconfig download error:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
status: 'error',
|
||||
message: 'Internal server error',
|
||||
details: error instanceof Error ? error.message : 'Unknown error',
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
162
app/api/services/deploy-vpn/route.ts
Normal file
162
app/api/services/deploy-vpn/route.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { authMiddleware } from '@/lib/auth-middleware'
|
||||
import connectDB from '@/lib/mongodb'
|
||||
import { User as UserModel } from '@/models/user'
|
||||
import BillingService from '@/lib/billing-service'
|
||||
|
||||
// Define your VPN endpoints
|
||||
const VPN_ENDPOINTS = {
|
||||
america: 'https://wireguard-vpn.3027622.siliconpin.com/vpn',
|
||||
europe: 'https://wireguard.vps20.siliconpin.com/vpn',
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
// Check authentication
|
||||
const user = await authMiddleware(request)
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
// Get data from request body
|
||||
const { orderId, location, plan, amount } = await request.json()
|
||||
|
||||
if (!orderId) {
|
||||
return NextResponse.json({ error: 'Order ID is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!location || !VPN_ENDPOINTS[location as keyof typeof VPN_ENDPOINTS]) {
|
||||
return NextResponse.json({ error: 'Valid VPN location is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Connect to MongoDB
|
||||
await connectDB()
|
||||
|
||||
// Get user data for billing
|
||||
const userData = await UserModel.findOne({ email: user.email })
|
||||
if (!userData) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
const requiredAmount = amount || 0
|
||||
|
||||
// Check balance only if amount > 0
|
||||
if (requiredAmount > 0) {
|
||||
const currentBalance = userData.balance || 0
|
||||
if (currentBalance < requiredAmount) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: `Insufficient balance. Required: ₹${requiredAmount}, Available: ₹${currentBalance}`,
|
||||
code: 'INSUFFICIENT_BALANCE',
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Get environment variables
|
||||
const VPN_API_KEY = process.env.VPN_API_KEY
|
||||
if (!VPN_API_KEY) {
|
||||
return NextResponse.json({ error: 'VPN API configuration missing' }, { status: 500 })
|
||||
}
|
||||
|
||||
// Get the endpoint for the selected location
|
||||
const endpoint = VPN_ENDPOINTS[location as keyof typeof VPN_ENDPOINTS]
|
||||
|
||||
// Make API request to the selected VPN endpoint
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-API-Key': VPN_API_KEY,
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
new: orderId.toString(),
|
||||
userId: user.id,
|
||||
plan,
|
||||
location,
|
||||
}),
|
||||
})
|
||||
console.log('VPN_API_KEY', VPN_API_KEY)
|
||||
// Handle non-200 responses
|
||||
if (!response.ok) {
|
||||
const errorData = await response.text()
|
||||
throw new Error(`VPN API returned HTTP ${response.status}: ${errorData}`)
|
||||
}
|
||||
|
||||
// Parse the response
|
||||
const vpnData = await response.json()
|
||||
const deploymentSuccess = response.ok && vpnData?.config
|
||||
|
||||
if (!deploymentSuccess) {
|
||||
throw new Error('Invalid response from VPN API: Missing config')
|
||||
}
|
||||
|
||||
// Process billing using the new comprehensive billing service
|
||||
try {
|
||||
const billingResult = await BillingService.processServiceDeployment({
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
siliconId: userData.siliconId,
|
||||
},
|
||||
service: {
|
||||
name: `VPN Service - ${location}`,
|
||||
type: 'vpn',
|
||||
id: orderId.toString(),
|
||||
config: {
|
||||
orderId,
|
||||
location,
|
||||
plan,
|
||||
endpoint: endpoint,
|
||||
},
|
||||
},
|
||||
amount: requiredAmount,
|
||||
currency: 'INR',
|
||||
cycle: 'monthly',
|
||||
deploymentSuccess,
|
||||
deploymentResponse: vpnData,
|
||||
metadata: {
|
||||
userAgent: request.headers.get('user-agent'),
|
||||
ip: request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip'),
|
||||
vpnLocation: location,
|
||||
vpnPlan: plan,
|
||||
},
|
||||
})
|
||||
|
||||
console.log('VPN billing processed:', {
|
||||
billingId: billingResult.billing.billing_id,
|
||||
transactionId: billingResult.transaction?.transactionId,
|
||||
balanceUpdated: billingResult.balanceUpdated,
|
||||
})
|
||||
} catch (billingError) {
|
||||
console.error('Billing processing failed:', billingError)
|
||||
// Continue even if billing fails, but return error if it's balance-related
|
||||
if (billingError instanceof Error && billingError.message.includes('Insufficient balance')) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: billingError.message,
|
||||
code: 'INSUFFICIENT_BALANCE',
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Return the VPN configuration
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: vpnData,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('VPN deployment error:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'VPN deployment failed',
|
||||
details: error instanceof Error ? error.message : 'Unknown error',
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
108
app/api/services/download-hosting-conf/route.ts
Normal file
108
app/api/services/download-hosting-conf/route.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { authMiddleware } from '@/lib/auth-middleware'
|
||||
import connectDB from '@/lib/mongodb'
|
||||
import { User as UserModel } from '@/models/user'
|
||||
import BillingService from '@/lib/billing-service'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
// Check authentication
|
||||
const user = await authMiddleware(request)
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
// Get billing ID from query parameters
|
||||
const { searchParams } = new URL(request.url)
|
||||
const billingId = searchParams.get('billing_id')
|
||||
const amount = parseFloat(searchParams.get('amount') || '0')
|
||||
|
||||
if (!billingId) {
|
||||
return NextResponse.json({ error: 'Billing ID is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Connect to MongoDB
|
||||
await connectDB()
|
||||
|
||||
// Get user data for billing
|
||||
const userData = await UserModel.findOne({ email: user.email })
|
||||
if (!userData) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
// Mock hosting configuration (in production, this would come from database)
|
||||
const hostingConfig = {
|
||||
billing_id: billingId,
|
||||
siliconId: user.id,
|
||||
domain: `demo-${billingId}.siliconpin.com`,
|
||||
cp_url: `https://cp-${billingId}.siliconpin.com:2083`,
|
||||
panel: 'cPanel',
|
||||
user_id: `user_${billingId}`,
|
||||
password: `secure_password_${Date.now()}`,
|
||||
server_ip: '192.168.1.100',
|
||||
nameservers: ['ns1.siliconpin.com', 'ns2.siliconpin.com'],
|
||||
ftp_settings: {
|
||||
host: `ftp.demo-${billingId}.siliconpin.com`,
|
||||
username: `user_${billingId}`,
|
||||
port: 21,
|
||||
},
|
||||
database_settings: {
|
||||
host: 'localhost',
|
||||
prefix: `db_${billingId}_`,
|
||||
},
|
||||
ssl_certificate: {
|
||||
enabled: true,
|
||||
type: "Let's Encrypt",
|
||||
auto_renew: true,
|
||||
},
|
||||
}
|
||||
|
||||
const configJson = JSON.stringify(hostingConfig, null, 2)
|
||||
|
||||
// Process billing for hosting configuration download
|
||||
try {
|
||||
await BillingService.processServiceDeployment({
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
siliconId: userData.siliconId,
|
||||
},
|
||||
service: {
|
||||
name: `Hosting Configuration - ${hostingConfig.domain}`,
|
||||
type: 'hosting',
|
||||
id: billingId,
|
||||
config: hostingConfig,
|
||||
},
|
||||
amount: amount,
|
||||
currency: 'INR',
|
||||
cycle: 'onetime',
|
||||
deploymentSuccess: true,
|
||||
deploymentResponse: { configDownloaded: true },
|
||||
metadata: {
|
||||
userAgent: request.headers.get('user-agent'),
|
||||
ip: request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip'),
|
||||
downloadType: 'hosting_config',
|
||||
domain: hostingConfig.domain,
|
||||
},
|
||||
})
|
||||
|
||||
console.log('Hosting config billing processed for:', billingId)
|
||||
} catch (billingError) {
|
||||
console.error('Billing processing failed:', billingError)
|
||||
// Continue with download even if billing fails
|
||||
}
|
||||
|
||||
// Return the config as a downloadable JSON file
|
||||
return new NextResponse(configJson, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Disposition': `attachment; filename="sp_credential_${billingId}.json"`,
|
||||
'Cache-Control': 'no-cache',
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Download error:', error)
|
||||
return NextResponse.json({ error: 'Download failed' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
54
app/api/services/download-kubernetes/route.ts
Normal file
54
app/api/services/download-kubernetes/route.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { authMiddleware } from '@/lib/auth-middleware'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
// Check authentication
|
||||
const user = await authMiddleware(request)
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
// Get cluster ID from query parameters
|
||||
const { searchParams } = new URL(request.url)
|
||||
const clusterId = searchParams.get('cluster_id')
|
||||
|
||||
if (!clusterId) {
|
||||
return NextResponse.json({ error: 'Cluster ID is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Mock Kubernetes configuration
|
||||
const kubeConfig = `apiVersion: v1
|
||||
clusters:
|
||||
- cluster:
|
||||
certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURCVENDQWUyZ0F3SUJBZ0lJRWRtTFUzZUNCUXN3RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB5TkRBeE1EUXhOekF6TXpCYUZ3MHpOREF4TURFeE56QXpNekJhTUJVeApFekFSQmdOVkJBTVRDbXQxWW1WeWJtVjBaWE13Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLLQEKQVFJREFRQUI=
|
||||
server: https://k8s-api.siliconpin.com:6443
|
||||
name: ${clusterId}
|
||||
contexts:
|
||||
- context:
|
||||
cluster: ${clusterId}
|
||||
user: ${clusterId}-admin
|
||||
name: ${clusterId}
|
||||
current-context: ${clusterId}
|
||||
kind: Config
|
||||
preferences: {}
|
||||
users:
|
||||
- name: ${clusterId}-admin
|
||||
user:
|
||||
client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0t
|
||||
client-key-data: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQ==`
|
||||
|
||||
// Return the config as a downloadable file
|
||||
return new NextResponse(kubeConfig, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'Content-Disposition': `attachment; filename="kubeconfig-${clusterId}.yaml"`,
|
||||
'Cache-Control': 'no-cache',
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Download error:', error)
|
||||
return NextResponse.json({ error: 'Download failed' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
285
app/api/services/hire-developer/route.ts
Normal file
285
app/api/services/hire-developer/route.ts
Normal file
@@ -0,0 +1,285 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { authMiddleware } from '@/lib/auth-middleware'
|
||||
import connectDB from '@/lib/mongodb'
|
||||
import { User as UserModel } from '@/models/user'
|
||||
import { DeveloperRequest, IDeveloperRequest } from '@/models/developer-request'
|
||||
import { Transaction } from '@/models/transaction'
|
||||
import BillingService from '@/lib/billing-service'
|
||||
import { checkServiceAvailability } from '@/lib/system-settings'
|
||||
|
||||
// Schema for developer hire request
|
||||
const HireDeveloperSchema = z.object({
|
||||
planId: z.enum(['hourly', 'daily', 'monthly']),
|
||||
planName: z.string().min(1),
|
||||
planPrice: z.number().positive(),
|
||||
requirements: z.string().min(10, 'Requirements must be at least 10 characters').max(5000),
|
||||
contactInfo: z.object({
|
||||
name: z.string().min(1, 'Name is required'),
|
||||
email: z.string().email('Valid email is required'),
|
||||
phone: z.string().optional(),
|
||||
}),
|
||||
})
|
||||
|
||||
// Schema for response
|
||||
const HireDeveloperResponseSchema = z.object({
|
||||
success: z.boolean(),
|
||||
data: z
|
||||
.object({
|
||||
requestId: z.string(),
|
||||
transactionId: z.string(),
|
||||
status: z.string(),
|
||||
message: z.string(),
|
||||
estimatedResponse: z.string(),
|
||||
})
|
||||
.optional(),
|
||||
error: z
|
||||
.object({
|
||||
message: z.string(),
|
||||
code: z.string(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
// Check if developer hire service is enabled
|
||||
if (!(await checkServiceAvailability('developer'))) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Developer hire service is currently disabled by administrator',
|
||||
code: 'SERVICE_DISABLED',
|
||||
},
|
||||
},
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
|
||||
// Authenticate user
|
||||
const user = await authMiddleware(request)
|
||||
if (!user) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: { message: 'Authentication required', code: 'UNAUTHORIZED' },
|
||||
},
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
await connectDB()
|
||||
|
||||
// Parse and validate request body
|
||||
const body = await request.json()
|
||||
const validatedData = HireDeveloperSchema.parse(body)
|
||||
|
||||
// Get user's current balance from database
|
||||
const userData = await UserModel.findOne({ email: user.email })
|
||||
if (!userData) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: { message: 'User not found', code: 'USER_NOT_FOUND' },
|
||||
},
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
const currentBalance = userData.balance || 0
|
||||
|
||||
// Check if user has sufficient balance (minimum deposit required)
|
||||
const minimumDeposit = Math.min(validatedData.planPrice * 0.5, 10000) // 50% or max ₹10,000
|
||||
if (currentBalance < minimumDeposit) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: `Insufficient balance. Minimum deposit required: ₹${minimumDeposit}, Available: ₹${currentBalance}`,
|
||||
code: 'INSUFFICIENT_BALANCE',
|
||||
},
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Create developer request first
|
||||
const developerRequest = new DeveloperRequest({
|
||||
userId: userData._id,
|
||||
planId: validatedData.planId,
|
||||
planName: validatedData.planName,
|
||||
planPrice: validatedData.planPrice,
|
||||
requirements: validatedData.requirements,
|
||||
contactInfo: validatedData.contactInfo,
|
||||
status: 'pending',
|
||||
paymentStatus: 'pending', // Will be updated after billing
|
||||
transactionId: null, // Will be set after transaction is created
|
||||
})
|
||||
|
||||
// Save developer request first
|
||||
const savedRequest = await developerRequest.save()
|
||||
|
||||
// Process billing using the new comprehensive billing service
|
||||
try {
|
||||
const billingResult = await BillingService.processServiceDeployment({
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
siliconId: userData.siliconId,
|
||||
},
|
||||
service: {
|
||||
name: `Developer Hire - ${validatedData.planName}`,
|
||||
type: 'developer_hire',
|
||||
id: savedRequest._id.toString(),
|
||||
config: {
|
||||
planId: validatedData.planId,
|
||||
planName: validatedData.planName,
|
||||
planPrice: validatedData.planPrice,
|
||||
requirements: validatedData.requirements,
|
||||
contactInfo: validatedData.contactInfo,
|
||||
},
|
||||
},
|
||||
amount: minimumDeposit,
|
||||
currency: 'INR',
|
||||
cycle: 'onetime',
|
||||
deploymentSuccess: true, // Developer hire request is always successful
|
||||
deploymentResponse: { requestId: savedRequest._id.toString() },
|
||||
metadata: {
|
||||
userAgent: request.headers.get('user-agent'),
|
||||
ip: request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip'),
|
||||
depositAmount: minimumDeposit,
|
||||
fullPlanPrice: validatedData.planPrice,
|
||||
},
|
||||
})
|
||||
|
||||
// Update developer request with billing info
|
||||
await (DeveloperRequest as any).updateOne(
|
||||
{ _id: savedRequest._id },
|
||||
{
|
||||
$set: {
|
||||
paymentStatus: 'paid',
|
||||
transactionId: billingResult.transaction?._id,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
console.log('Developer hire billing processed:', {
|
||||
requestId: savedRequest._id,
|
||||
billingId: billingResult.billing.billing_id,
|
||||
transactionId: billingResult.transaction?.transactionId,
|
||||
})
|
||||
} catch (billingError) {
|
||||
console.error('Billing processing failed:', billingError)
|
||||
// Rollback developer request if billing fails
|
||||
await (DeveloperRequest as any).deleteOne({ _id: savedRequest._id })
|
||||
|
||||
if (billingError instanceof Error && billingError.message.includes('Insufficient balance')) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: billingError.message,
|
||||
code: 'INSUFFICIENT_BALANCE',
|
||||
},
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
throw new Error('Failed to process payment')
|
||||
}
|
||||
|
||||
const responseData = {
|
||||
success: true,
|
||||
data: {
|
||||
requestId: savedRequest._id.toString(),
|
||||
transactionId: 'processed_via_billing',
|
||||
status: 'pending',
|
||||
message:
|
||||
'Your developer hire request has been submitted successfully! Our team will review your requirements and contact you within 24 hours.',
|
||||
estimatedResponse: '24 hours',
|
||||
},
|
||||
}
|
||||
|
||||
const validatedResponse = HireDeveloperResponseSchema.parse(responseData)
|
||||
return NextResponse.json(validatedResponse, { status: 200 })
|
||||
} catch (error) {
|
||||
console.error('Developer hire error:', error)
|
||||
|
||||
if (error instanceof z.ZodError) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Invalid request data',
|
||||
code: 'VALIDATION_ERROR',
|
||||
details: error.issues,
|
||||
},
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: { message: 'Failed to process developer hire request', code: 'INTERNAL_ERROR' },
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// GET endpoint to fetch user's developer requests
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const user = await authMiddleware(request)
|
||||
if (!user) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: { message: 'Authentication required', code: 'UNAUTHORIZED' },
|
||||
},
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
await connectDB()
|
||||
|
||||
const userData = await UserModel.findOne({ email: user.email })
|
||||
if (!userData) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: { message: 'User not found', code: 'USER_NOT_FOUND' },
|
||||
},
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
// Get user's developer requests
|
||||
const requests = await (DeveloperRequest as any)
|
||||
.find({ userId: userData._id })
|
||||
.sort({ createdAt: -1 })
|
||||
.populate('transactionId')
|
||||
.lean()
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
requests,
|
||||
total: requests.length,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch developer requests:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: { message: 'Failed to fetch requests', code: 'INTERNAL_ERROR' },
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
52
app/api/startup-test/route.ts
Normal file
52
app/api/startup-test/route.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { testMongoConnection } from '@/lib/mongodb'
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
console.log('🚀 Running startup connection tests...')
|
||||
|
||||
const mongoStatus = await testMongoConnection()
|
||||
|
||||
// Test Redis connection (basic check)
|
||||
let redisStatus = false
|
||||
try {
|
||||
// Import redis client
|
||||
const { redisClient } = await import('@/lib/redis')
|
||||
await redisClient.ping()
|
||||
console.log('✅ Redis connection test successful')
|
||||
redisStatus = true
|
||||
} catch (error) {
|
||||
console.error('❌ Redis connection test failed:', error)
|
||||
console.error('Redis is optional but recommended for session storage')
|
||||
}
|
||||
|
||||
const overallStatus = mongoStatus // Redis is optional, so we only require MongoDB
|
||||
|
||||
if (overallStatus) {
|
||||
console.log('🎉 All critical services are connected and ready!')
|
||||
} else {
|
||||
console.log('⚠️ Some services failed connection tests')
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: overallStatus,
|
||||
services: {
|
||||
mongodb: mongoStatus,
|
||||
redis: redisStatus,
|
||||
},
|
||||
message: overallStatus
|
||||
? 'All critical services connected successfully'
|
||||
: 'Some services failed connection tests - check logs',
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('❌ Startup test failed:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Startup test failed',
|
||||
message: 'Check server logs for details',
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
76
app/api/tags/route.ts
Normal file
76
app/api/tags/route.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import TopicModel from '@/models/topic'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const searchParams = request.nextUrl.searchParams
|
||||
const includeStats = searchParams.get('stats') === 'true'
|
||||
|
||||
// Get all published topics and extract tags
|
||||
const topics = await TopicModel.find({ isDraft: false })
|
||||
.select('tags')
|
||||
.lean()
|
||||
|
||||
if (!topics || topics.length === 0) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
data: [],
|
||||
},
|
||||
{ status: 200 }
|
||||
)
|
||||
}
|
||||
|
||||
// Create a map to track tag usage
|
||||
const tagMap = new Map<string, { name: string; count: number }>()
|
||||
|
||||
topics.forEach((topic) => {
|
||||
topic.tags?.forEach((tag: { id?: string; name: string }) => {
|
||||
const tagName = tag.name
|
||||
const existing = tagMap.get(tagName.toLowerCase())
|
||||
|
||||
if (existing) {
|
||||
existing.count++
|
||||
} else {
|
||||
tagMap.set(tagName.toLowerCase(), {
|
||||
name: tagName,
|
||||
count: 1,
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// Convert to array and sort by usage count (descending)
|
||||
const tagsArray = Array.from(tagMap.values())
|
||||
.sort((a, b) => b.count - a.count)
|
||||
|
||||
// If stats are requested, include the count
|
||||
const result = tagsArray.map((tag, index) => ({
|
||||
id: `tag-${index + 1}`,
|
||||
name: tag.name,
|
||||
...(includeStats && { count: tag.count }),
|
||||
}))
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
data: result,
|
||||
meta: {
|
||||
totalTags: result.length,
|
||||
totalTopics: topics.length,
|
||||
},
|
||||
},
|
||||
{ status: 200 }
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('Error fetching tags:', error)
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: { message: 'Failed to fetch tags', code: 'SERVER_ERROR' },
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
71
app/api/tools/openai-chat/route.ts
Normal file
71
app/api/tools/openai-chat/route.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { authMiddleware } from '@/lib/auth-middleware'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
// No authentication required for web-speech tool
|
||||
|
||||
const { message, systemPrompt } = await request.json()
|
||||
|
||||
if (!message || typeof message !== 'string') {
|
||||
return NextResponse.json({ error: 'Message is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Check if OpenAI API key is configured
|
||||
if (!process.env.OPENAI_API_KEY) {
|
||||
return NextResponse.json({ error: 'OpenAI API key not configured' }, { status: 500 })
|
||||
}
|
||||
|
||||
// Prepare messages for OpenAI
|
||||
const messages = [
|
||||
{
|
||||
role: 'system',
|
||||
content:
|
||||
systemPrompt ||
|
||||
'You are a helpful AI assistant. Provide clear, concise, and helpful responses to user queries.',
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: message,
|
||||
},
|
||||
]
|
||||
|
||||
// Call OpenAI Chat Completions API
|
||||
const response = await fetch('https://api.openai.com/v1/chat/completions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'gpt-3.5-turbo',
|
||||
messages: messages,
|
||||
max_tokens: 1000,
|
||||
temperature: 0.7,
|
||||
top_p: 1,
|
||||
frequency_penalty: 0,
|
||||
presence_penalty: 0,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.text()
|
||||
console.error('OpenAI Chat API error:', errorData)
|
||||
return NextResponse.json({ error: 'AI processing failed' }, { status: 500 })
|
||||
}
|
||||
|
||||
const chatResult = await response.json()
|
||||
|
||||
if (!chatResult.choices || chatResult.choices.length === 0) {
|
||||
return NextResponse.json({ error: 'No response from AI' }, { status: 500 })
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
response: chatResult.choices[0].message.content,
|
||||
usage: chatResult.usage,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('OpenAI chat error:', error)
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
166
app/api/topic-content-image/route.ts
Normal file
166
app/api/topic-content-image/route.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { authMiddleware } from '@/lib/auth-middleware'
|
||||
import { uploadFile, moveToPermStorage, getFileUrl, generateUniqueFilename } from '@/lib/file-vault'
|
||||
|
||||
// POST /api/topic-content-image - Upload images for topic content (rich text editor)
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
// Authentication required for image uploads
|
||||
const user = await authMiddleware(request)
|
||||
if (!user) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: { message: 'Authentication required', code: 'AUTH_REQUIRED' },
|
||||
},
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const formData = await request.formData()
|
||||
const image = formData.get('file') as File
|
||||
|
||||
if (!image) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: { message: 'Image file is required', code: 'MISSING_IMAGE' },
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Validate file type
|
||||
const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp']
|
||||
if (!allowedTypes.includes(image.type)) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Invalid file type. Only JPEG, PNG, GIF, and WebP are allowed',
|
||||
code: 'INVALID_FILE_TYPE',
|
||||
},
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Validate file size (5MB limit)
|
||||
const maxSize = 5 * 1024 * 1024 // 5MB
|
||||
if (image.size > maxSize) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: 'File size too large. Maximum size is 5MB',
|
||||
code: 'FILE_TOO_LARGE',
|
||||
},
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
console.log('Processing topic content image upload for user:', user.id)
|
||||
|
||||
try {
|
||||
// Convert file to buffer (exactly like dev-portfolio)
|
||||
const bytes = await image.arrayBuffer()
|
||||
const buffer = Buffer.from(bytes)
|
||||
|
||||
// Upload directly to external API (no temp storage needed)
|
||||
const uploadResult = await uploadFile(buffer, image.name, image.type, user.id)
|
||||
const imageUrl = uploadResult.url
|
||||
const uniqueFilename = uploadResult.filename
|
||||
const permanentPath = uploadResult.url
|
||||
|
||||
console.log('Topic content image uploaded successfully:', {
|
||||
originalName: image.name,
|
||||
permanentPath,
|
||||
uploadedBy: user.id,
|
||||
})
|
||||
|
||||
// Return URL format expected by BlockNote editor
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
data: {
|
||||
url: imageUrl, // BlockNote expects URL here
|
||||
fileName: uniqueFilename,
|
||||
path: permanentPath,
|
||||
size: image.size,
|
||||
type: image.type,
|
||||
uploadedBy: user.id,
|
||||
uploadedAt: new Date().toISOString(),
|
||||
},
|
||||
},
|
||||
{ status: 200 }
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('Failed to upload topic content image:', error)
|
||||
throw error
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error uploading topic content image:', error)
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: { message: 'Failed to upload image', code: 'UPLOAD_ERROR' },
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/topic-content-image - Get image metadata or URL by path (like dev-portfolio)
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
// Authentication required to access image metadata
|
||||
const user = await authMiddleware(request)
|
||||
if (!user) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: { message: 'Authentication required', code: 'AUTH_REQUIRED' },
|
||||
},
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url)
|
||||
const imagePath = searchParams.get('path')
|
||||
|
||||
if (!imagePath) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: { message: 'Image path parameter is required', code: 'MISSING_PATH' },
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Get accessible URL for the image (like dev-portfolio)
|
||||
const imageUrl = await getFileUrl(imagePath)
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
data: {
|
||||
path: imagePath,
|
||||
url: imageUrl,
|
||||
},
|
||||
},
|
||||
{ status: 200 }
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('Error serving topic content image:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: { message: 'Failed to retrieve image', code: 'SERVER_ERROR' },
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
261
app/api/topic/[id]/route.ts
Normal file
261
app/api/topic/[id]/route.ts
Normal file
@@ -0,0 +1,261 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import TopicModel, { transformToTopic } from '@/models/topic'
|
||||
import { authMiddleware } from '@/lib/auth-middleware'
|
||||
import { uploadFile, moveToPermStorage, getFileUrl, generateUniqueFilename } from '@/lib/file-vault'
|
||||
import { z } from 'zod'
|
||||
|
||||
// Validation schema for topic updates
|
||||
const topicUpdateSchema = z.object({
|
||||
title: z.string().min(1).max(100).optional(),
|
||||
author: z.string().min(1).optional(),
|
||||
excerpt: z.string().min(1).max(200).optional(),
|
||||
content: z.string().min(1).optional(),
|
||||
contentRTE: z.unknown().optional(),
|
||||
contentImages: z.array(z.string()).optional(),
|
||||
tags: z
|
||||
.array(
|
||||
z.object({
|
||||
id: z.string().optional(),
|
||||
name: z.string(),
|
||||
})
|
||||
)
|
||||
.min(1)
|
||||
.optional(),
|
||||
featured: z.boolean().optional(),
|
||||
isDraft: z.boolean().optional(),
|
||||
coverImage: z.string().url().optional(),
|
||||
coverImageKey: z.string().optional(),
|
||||
})
|
||||
|
||||
// GET /api/topic/[id] - Get topic by ID
|
||||
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
try {
|
||||
const { id } = await params
|
||||
|
||||
const topic = await TopicModel.findOne({ id }).lean()
|
||||
|
||||
if (!topic) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: { message: 'Topic not found', code: 'NOT_FOUND' },
|
||||
},
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
// Check if topic is draft and user is not the owner
|
||||
if (topic.isDraft) {
|
||||
const user = await authMiddleware(request)
|
||||
if (!user || user.id !== topic.authorId) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: { message: 'Topic not found', code: 'NOT_FOUND' },
|
||||
},
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const transformedTopic = transformToTopic(topic)
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
data: transformedTopic,
|
||||
},
|
||||
{ status: 200 }
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('Error fetching topic:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: { message: 'Failed to fetch topic', code: 'SERVER_ERROR' },
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// PUT /api/topic/[id] - Update topic by ID
|
||||
export async function PUT(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
try {
|
||||
// Authentication required for updating topics
|
||||
const user = await authMiddleware(request)
|
||||
if (!user) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: { message: 'Authentication required', code: 'AUTH_REQUIRED' },
|
||||
},
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const { id } = await params
|
||||
|
||||
// Find the topic first
|
||||
const existingTopic = await TopicModel.findOne({ id })
|
||||
|
||||
if (!existingTopic) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: { message: 'Topic not found', code: 'NOT_FOUND' },
|
||||
},
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
// Check ownership - users can only update their own topics
|
||||
if (existingTopic.authorId !== user.id) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: { message: 'You can only edit your own topics', code: 'FORBIDDEN' },
|
||||
},
|
||||
{ status: 403 }
|
||||
)
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
|
||||
// Validate request body
|
||||
const validatedData = topicUpdateSchema.parse(body)
|
||||
|
||||
console.log('Updating topic for user:', user.id, 'Topic ID:', id)
|
||||
|
||||
try {
|
||||
// Update the topic with new data
|
||||
Object.assign(existingTopic, validatedData)
|
||||
existingTopic.publishedAt = Date.now() // Update timestamp
|
||||
|
||||
const updatedTopic = await existingTopic.save()
|
||||
|
||||
console.log('Topic updated successfully:', updatedTopic.id)
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
data: updatedTopic,
|
||||
},
|
||||
{ status: 200 }
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('Failed to update topic:', error)
|
||||
|
||||
if (error.code === 11000) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: { message: 'A topic with this slug already exists', code: 'DUPLICATE_SLUG' },
|
||||
},
|
||||
{ status: 409 }
|
||||
)
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating topic:', error)
|
||||
|
||||
if (error instanceof z.ZodError) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Validation failed',
|
||||
code: 'VALIDATION_ERROR',
|
||||
details: error.issues,
|
||||
},
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: { message: 'Failed to update topic', code: 'SERVER_ERROR' },
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE /api/topic/[id] - Delete topic by ID
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
// Authentication required for deleting topics
|
||||
const user = await authMiddleware(request)
|
||||
if (!user) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: { message: 'Authentication required', code: 'AUTH_REQUIRED' },
|
||||
},
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const { id } = await params
|
||||
|
||||
// Find the topic first
|
||||
const existingTopic = await TopicModel.findOne({ id })
|
||||
|
||||
if (!existingTopic) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: { message: 'Topic not found', code: 'NOT_FOUND' },
|
||||
},
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
// Check ownership - users can only delete their own topics
|
||||
if (existingTopic.authorId !== user.id) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: { message: 'You can only delete your own topics', code: 'FORBIDDEN' },
|
||||
},
|
||||
{ status: 403 }
|
||||
)
|
||||
}
|
||||
|
||||
console.log('Deleting topic for user:', user.id, 'Topic ID:', id)
|
||||
|
||||
try {
|
||||
await TopicModel.deleteOne({ id })
|
||||
|
||||
console.log('Topic deleted successfully:', id)
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
message: 'Topic deleted successfully',
|
||||
},
|
||||
{ status: 200 }
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('Failed to delete topic:', error)
|
||||
throw error
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting topic:', error)
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: { message: 'Failed to delete topic', code: 'SERVER_ERROR' },
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
427
app/api/topic/route.ts
Normal file
427
app/api/topic/route.ts
Normal file
@@ -0,0 +1,427 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { randomUUID } from 'crypto'
|
||||
import TopicModel from '@/models/topic'
|
||||
import { authMiddleware } from '@/lib/auth-middleware'
|
||||
import { uploadFile, moveToPermStorage, getFileUrl, generateUniqueFilename } from '@/lib/file-vault'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
console.log('Starting topic post processing')
|
||||
|
||||
// Debug authentication data
|
||||
const authHeader = request.headers.get('authorization')
|
||||
const accessTokenCookie = request.cookies.get('accessToken')?.value
|
||||
console.log('🔍 Auth debug:', {
|
||||
hasAuthHeader: !!authHeader,
|
||||
authHeaderPrefix: authHeader?.substring(0, 20),
|
||||
hasAccessTokenCookie: !!accessTokenCookie,
|
||||
cookiePrefix: accessTokenCookie?.substring(0, 20),
|
||||
allCookies: Object.fromEntries(
|
||||
request.cookies.getAll().map((c) => [c.name, c.value?.substring(0, 20)])
|
||||
),
|
||||
})
|
||||
|
||||
// Authentication required
|
||||
const user = await authMiddleware(request)
|
||||
console.log('🔍 Auth result:', { user: user ? { id: user.id, email: user.email } : null })
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: { message: 'Authentication required', code: 'AUTH_REQUIRED' },
|
||||
},
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const formData = await request.formData()
|
||||
const coverImage = formData.get('coverImage') as File
|
||||
const topicId = request.nextUrl.searchParams.get('topicId')
|
||||
console.log('Form data received', { topicId, hasCoverImage: !!coverImage })
|
||||
|
||||
// Get other form data with proper validation
|
||||
const title = formData.get('title') as string
|
||||
const author = formData.get('author') as string
|
||||
const excerpt = formData.get('excerpt') as string
|
||||
const content = formData.get('content') as string
|
||||
const contentRTE = formData.get('contentRTE') as string
|
||||
const featured = formData.get('featured') === 'true'
|
||||
// Get the LAST isDraft value (in case of duplicates)
|
||||
const allIsDraftValues = formData.getAll('isDraft')
|
||||
console.log('🐛 All isDraft values in FormData:', allIsDraftValues)
|
||||
const isDraft = allIsDraftValues[allIsDraftValues.length - 1] === 'true'
|
||||
console.log('🐛 Final isDraft value:', isDraft)
|
||||
|
||||
// Parse JSON fields safely (like dev-portfolio)
|
||||
let contentImages: string[] = []
|
||||
let tags: any[] = []
|
||||
|
||||
try {
|
||||
const contentImagesStr = formData.get('contentImages') as string
|
||||
contentImages = contentImagesStr ? JSON.parse(contentImagesStr) : []
|
||||
} catch (error) {
|
||||
console.warn('Failed to parse contentImages, using empty array:', error)
|
||||
contentImages = []
|
||||
}
|
||||
|
||||
try {
|
||||
const tagsStr = formData.get('tags') as string
|
||||
tags = tagsStr ? JSON.parse(tagsStr) : []
|
||||
} catch (error) {
|
||||
console.warn('Failed to parse tags, using empty array:', error)
|
||||
tags = []
|
||||
}
|
||||
|
||||
// Validate required fields (like dev-portfolio)
|
||||
if (!title?.trim()) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: { message: 'Title is required', code: 'VALIDATION_ERROR' } },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
if (!excerpt?.trim()) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: { message: 'Excerpt is required', code: 'VALIDATION_ERROR' } },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
if (!content?.trim()) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: { message: 'Content is required', code: 'VALIDATION_ERROR' } },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
if (!Array.isArray(tags) || tags.length === 0) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: { message: 'At least one tag is required', code: 'VALIDATION_ERROR' },
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const targetTopicId = topicId ? topicId : randomUUID()
|
||||
|
||||
// For existing topic, fetch the current data
|
||||
let existingTopic = null
|
||||
|
||||
if (topicId) {
|
||||
existingTopic = await TopicModel.findOne({ id: topicId })
|
||||
if (!existingTopic) {
|
||||
return NextResponse.json({ success: false, message: 'Topic not found' }, { status: 404 })
|
||||
}
|
||||
}
|
||||
|
||||
// Handle cover image upload exactly like dev-portfolio
|
||||
let tempImageResult = null
|
||||
if (coverImage && coverImage instanceof File) {
|
||||
try {
|
||||
console.log('🖼️ Processing cover image upload:', {
|
||||
name: coverImage.name,
|
||||
size: coverImage.size,
|
||||
type: coverImage.type,
|
||||
})
|
||||
|
||||
// Upload using external API
|
||||
try {
|
||||
// Convert file to buffer
|
||||
const bytes = await coverImage.arrayBuffer()
|
||||
const buffer = Buffer.from(bytes)
|
||||
|
||||
console.log('📤 Buffer created, starting upload...')
|
||||
|
||||
// Upload directly to external API (no temp storage needed)
|
||||
const uploadResult = await uploadFile(buffer, coverImage.name, coverImage.type, user.id)
|
||||
const coverImageUrl = uploadResult.url
|
||||
const permanentPath = uploadResult.url
|
||||
|
||||
tempImageResult = {
|
||||
coverImage: coverImageUrl,
|
||||
coverImageKey: permanentPath,
|
||||
}
|
||||
|
||||
console.log('✅ Cover image uploaded successfully:', { permanentPath })
|
||||
} catch (uploadError) {
|
||||
console.error('❌ Cover image upload failed:', uploadError)
|
||||
throw new Error(`Failed to upload cover image: ${uploadError.message}`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error processing cover image:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const dataToSave = {
|
||||
id: targetTopicId,
|
||||
publishedAt: Date.now(),
|
||||
title,
|
||||
author,
|
||||
authorId: user.id, // Add user ownership
|
||||
excerpt,
|
||||
content,
|
||||
contentRTE: contentRTE ? JSON.parse(contentRTE) : [],
|
||||
contentImages,
|
||||
featured,
|
||||
tags,
|
||||
isDraft,
|
||||
// Use uploaded image or existing image for updates, require image for new topics like dev-portfolio
|
||||
coverImage:
|
||||
tempImageResult?.coverImage ||
|
||||
existingTopic?.coverImage ||
|
||||
'https://via.placeholder.com/800x400',
|
||||
coverImageKey: tempImageResult?.coverImageKey || existingTopic?.coverImageKey || '',
|
||||
}
|
||||
console.log('Preparing to save topic data', { targetTopicId })
|
||||
|
||||
let savedArticle
|
||||
try {
|
||||
if (existingTopic) {
|
||||
// Update existing topic
|
||||
console.log('Updating existing topic')
|
||||
Object.assign(existingTopic, dataToSave)
|
||||
savedArticle = await existingTopic.save()
|
||||
} else {
|
||||
// Create new topic
|
||||
console.log('Creating new topic')
|
||||
const newArticle = new TopicModel(dataToSave)
|
||||
savedArticle = await newArticle.save()
|
||||
}
|
||||
console.log('Topic saved successfully', { topicId: savedArticle.id })
|
||||
} catch (error) {
|
||||
console.error('Failed to save topic', { error })
|
||||
throw error
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
data: savedArticle,
|
||||
},
|
||||
{ status: 200 }
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('Error processing topic post:', {
|
||||
error,
|
||||
message: error.message,
|
||||
})
|
||||
return NextResponse.json(
|
||||
{ success: false, message: 'Failed to process topic post', error: error.message },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const page = parseInt(searchParams.get('page') || '1')
|
||||
const limit = parseInt(searchParams.get('limit') || '10')
|
||||
const featured = searchParams.get('featured') === 'true'
|
||||
const isDraft = searchParams.get('isDraft')
|
||||
const search = searchParams.get('search')
|
||||
const tag = searchParams.get('tag')
|
||||
|
||||
// Build query (like dev-portfolio)
|
||||
const query: any = {}
|
||||
|
||||
// Featured filter
|
||||
if (featured) query.featured = true
|
||||
|
||||
// Draft filter (only if explicitly set)
|
||||
if (isDraft !== null) query.isDraft = isDraft === 'true'
|
||||
|
||||
// Search functionality (like dev-portfolio)
|
||||
if (search) {
|
||||
query.$or = [
|
||||
{ title: { $regex: search, $options: 'i' } },
|
||||
{ excerpt: { $regex: search, $options: 'i' } },
|
||||
{ content: { $regex: search, $options: 'i' } },
|
||||
]
|
||||
}
|
||||
|
||||
// Tag filtering (like dev-portfolio)
|
||||
if (tag) {
|
||||
query['tags.name'] = { $regex: tag, $options: 'i' }
|
||||
}
|
||||
|
||||
const skip = (page - 1) * limit
|
||||
|
||||
const [topics, total] = await Promise.all([
|
||||
TopicModel.find(query).sort({ publishedAt: -1 }).skip(skip).limit(limit),
|
||||
TopicModel.countDocuments(query),
|
||||
])
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
topics,
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
pages: Math.ceil(total / limit),
|
||||
},
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error fetching topics:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: { message: 'Failed to fetch topics', code: 'SERVER_ERROR' },
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// PUT /api/topic - Update existing topic (like dev-portfolio)
|
||||
export async function PUT(request: NextRequest) {
|
||||
try {
|
||||
console.log('Starting topic update processing')
|
||||
|
||||
// Authentication required
|
||||
const user = await authMiddleware(request)
|
||||
if (!user) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: { message: 'Authentication required', code: 'AUTH_REQUIRED' },
|
||||
},
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const formData = await request.formData()
|
||||
const topicId = formData.get('topicId') as string
|
||||
|
||||
if (!topicId) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: { message: 'Topic ID is required for updates', code: 'MISSING_TOPIC_ID' },
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Find existing topic and check ownership
|
||||
const existingTopic = await TopicModel.findOne({ id: topicId })
|
||||
if (!existingTopic) {
|
||||
return NextResponse.json({ success: false, message: 'Topic not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
// Check authorization - user can only update their own topics
|
||||
if (existingTopic.authorId !== user.id) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: { message: 'Not authorized to update this topic', code: 'UNAUTHORIZED' },
|
||||
},
|
||||
{ status: 403 }
|
||||
)
|
||||
}
|
||||
|
||||
// Use the same logic as POST but for updates
|
||||
// This reuses the form processing logic from POST
|
||||
const url = new URL(request.url)
|
||||
url.searchParams.set('topicId', topicId)
|
||||
|
||||
// Create new request for POST handler with topicId parameter
|
||||
const updateRequest = new NextRequest(url.toString(), {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: request.headers,
|
||||
})
|
||||
|
||||
return POST(updateRequest)
|
||||
} catch (error) {
|
||||
console.error('Error updating topic post:', error)
|
||||
return NextResponse.json(
|
||||
{ success: false, message: 'Failed to update topic post', error: error.message },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE /api/topic - Delete topic (like dev-portfolio)
|
||||
export async function DELETE(request: NextRequest) {
|
||||
try {
|
||||
console.log('Starting topic deletion')
|
||||
|
||||
// Authentication required
|
||||
const user = await authMiddleware(request)
|
||||
if (!user) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: { message: 'Authentication required', code: 'AUTH_REQUIRED' },
|
||||
},
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url)
|
||||
const topicId = searchParams.get('id')
|
||||
|
||||
if (!topicId) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: { message: 'Topic ID is required', code: 'MISSING_TOPIC_ID' } },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Find existing topic and check ownership
|
||||
const existingTopic = await TopicModel.findOne({ id: topicId })
|
||||
if (!existingTopic) {
|
||||
return NextResponse.json({ success: false, message: 'Topic not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
// Check authorization - user can only delete their own topics
|
||||
if (existingTopic.authorId !== user.id) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: { message: 'Not authorized to delete this topic', code: 'UNAUTHORIZED' },
|
||||
},
|
||||
{ status: 403 }
|
||||
)
|
||||
}
|
||||
|
||||
console.log('Deleting topic:', { topicId, title: existingTopic.title?.substring(0, 50) })
|
||||
|
||||
// Delete the topic
|
||||
await TopicModel.deleteOne({ id: topicId })
|
||||
|
||||
console.log('Topic deleted successfully:', topicId)
|
||||
|
||||
// TODO: Implement image cleanup like dev-portfolio
|
||||
// if (existingTopic.coverImageKey && existingTopic.coverImageKey !== 'placeholder-key') {
|
||||
// await deleteFile(existingTopic.coverImageKey)
|
||||
// }
|
||||
// if (existingTopic.contentImages && existingTopic.contentImages.length > 0) {
|
||||
// for (const imagePath of existingTopic.contentImages) {
|
||||
// await deleteFile(imagePath)
|
||||
// }
|
||||
// }
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
message: 'Topic deleted successfully',
|
||||
data: { id: topicId },
|
||||
},
|
||||
{ status: 200 }
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('Error deleting topic:', error)
|
||||
return NextResponse.json(
|
||||
{ success: false, message: 'Failed to delete topic', error: error.message },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
78
app/api/topic/slug/[slug]/route.ts
Normal file
78
app/api/topic/slug/[slug]/route.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import TopicModel, { transformToTopic } from '@/models/topic'
|
||||
import { authMiddleware } from '@/lib/auth-middleware'
|
||||
|
||||
// GET /api/topic/slug/[slug] - Get topic by slug (for public viewing)
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ slug: string }> }
|
||||
) {
|
||||
try {
|
||||
const { slug } = await params
|
||||
|
||||
if (!slug) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: { message: 'Slug parameter is required', code: 'MISSING_SLUG' },
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const topic = await TopicModel.findOne({ slug }).lean()
|
||||
|
||||
if (!topic) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: { message: 'Topic not found', code: 'NOT_FOUND' },
|
||||
},
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
// Check if topic is draft and user is not the owner
|
||||
if (topic.isDraft) {
|
||||
const user = await authMiddleware(request)
|
||||
if (!user || user.id !== topic.authorId) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: { message: 'Topic not found', code: 'NOT_FOUND' },
|
||||
},
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const transformedTopic = transformToTopic(topic)
|
||||
|
||||
if (!transformedTopic) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: { message: 'Failed to process topic data', code: 'PROCESSING_ERROR' },
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
data: transformedTopic,
|
||||
},
|
||||
{ status: 200 }
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('Error fetching topic by slug:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: { message: 'Failed to fetch topic', code: 'SERVER_ERROR' },
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
71
app/api/topics/[slug]/related/route.ts
Normal file
71
app/api/topics/[slug]/related/route.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import connectDB from '@/lib/mongodb'
|
||||
import TopicModel, { transformToTopics } from '@/models/topic'
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ slug: string }> }
|
||||
) {
|
||||
try {
|
||||
await connectDB()
|
||||
|
||||
const { slug } = await params
|
||||
const searchParams = request.nextUrl.searchParams
|
||||
const limit = parseInt(searchParams.get('limit') || '2')
|
||||
|
||||
if (!slug) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Topic slug is required',
|
||||
code: 'SLUG_REQUIRED',
|
||||
},
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// First, get the current post to find its ID and tags
|
||||
const currentPost = await TopicModel.findOne({
|
||||
slug,
|
||||
isDraft: false
|
||||
}).lean()
|
||||
|
||||
if (!currentPost) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: [],
|
||||
})
|
||||
}
|
||||
|
||||
// Find related posts by excluding the current post and sorting by publishedAt
|
||||
const relatedPosts = await TopicModel.find({
|
||||
id: { $ne: currentPost.id },
|
||||
isDraft: false,
|
||||
})
|
||||
.sort({ publishedAt: -1 })
|
||||
.limit(limit)
|
||||
.lean()
|
||||
|
||||
// Transform to proper format
|
||||
const transformedPosts = transformToTopics(relatedPosts)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: transformedPosts,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error fetching related posts:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Failed to fetch related posts',
|
||||
code: 'SERVER_ERROR',
|
||||
},
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
79
app/api/topics/[slug]/route.ts
Normal file
79
app/api/topics/[slug]/route.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import connectDB from '@/lib/mongodb'
|
||||
import TopicModel, { transformToTopic } from '@/models/topic'
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ slug: string }> }
|
||||
) {
|
||||
try {
|
||||
await connectDB()
|
||||
|
||||
const { slug } = await params
|
||||
|
||||
if (!slug) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Topic slug is required',
|
||||
code: 'SLUG_REQUIRED',
|
||||
},
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Find the topic post by slug (only published posts)
|
||||
const post = await TopicModel.findOne({
|
||||
slug,
|
||||
isDraft: false
|
||||
}).lean()
|
||||
|
||||
if (!post) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Topic post not found',
|
||||
code: 'POST_NOT_FOUND',
|
||||
},
|
||||
},
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
// Transform to proper format
|
||||
const transformedPost = transformToTopic(post)
|
||||
|
||||
if (!transformedPost) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Failed to process topic post',
|
||||
code: 'PROCESSING_ERROR',
|
||||
},
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: transformedPost,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error fetching topic post:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Failed to fetch topic post',
|
||||
code: 'SERVER_ERROR',
|
||||
},
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
75
app/api/topics/[slug]/view/route.ts
Normal file
75
app/api/topics/[slug]/view/route.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import connectDB from '@/lib/mongodb'
|
||||
import TopicModel from '@/models/topic'
|
||||
|
||||
// Track topic view (increment view count and daily analytics)
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ slug: string }> }
|
||||
) {
|
||||
try {
|
||||
const { slug } = await params
|
||||
|
||||
if (!slug) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: { message: 'Topic slug is required', code: 'MISSING_SLUG' },
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
await connectDB()
|
||||
|
||||
// Get current date in YYYY-MM-DD format
|
||||
const today = new Date().toISOString().split('T')[0]
|
||||
|
||||
// Find and update the topic
|
||||
const topic = await TopicModel.findOneAndUpdate(
|
||||
{ slug: slug, isDraft: false }, // Only track views for published posts
|
||||
{
|
||||
$inc: { views: 1 }, // Increment total views
|
||||
$push: {
|
||||
viewHistory: {
|
||||
$each: [{ date: today, count: 1 }],
|
||||
$slice: -30, // Keep only last 30 days
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
new: true,
|
||||
projection: { views: 1, slug: 1, title: 1, authorId: 1 } // Include authorId for cache invalidation
|
||||
}
|
||||
)
|
||||
|
||||
if (!topic) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: { message: 'Topic not found or is a draft', code: 'TOPIC_NOT_FOUND' },
|
||||
},
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
slug: topic.slug,
|
||||
title: topic.title,
|
||||
views: topic.views,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('View tracking error:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: { message: 'Failed to track view', code: 'INTERNAL_ERROR' },
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
142
app/api/topics/route.ts
Normal file
142
app/api/topics/route.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import connectDB from '@/lib/mongodb'
|
||||
import TopicModel, { transformToTopics } from '@/models/topic'
|
||||
import { authMiddleware } from '@/lib/auth-middleware'
|
||||
import { ZodError } from 'zod'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
await connectDB()
|
||||
const searchParams = request.nextUrl.searchParams
|
||||
const page = parseInt(searchParams.get('page') || '1')
|
||||
const limit = parseInt(searchParams.get('limit') || '10')
|
||||
const search = searchParams.get('q') || ''
|
||||
const tag = searchParams.get('tag') || ''
|
||||
const authorId = searchParams.get('authorId') || '' // For filtering by user's topics
|
||||
const includeDrafts = searchParams.get('includeDrafts') === 'true' // For user's own topics
|
||||
const fetchAll = searchParams.get('fetchAll') // Admin panel support
|
||||
const skip = (page - 1) * limit
|
||||
|
||||
// Build query based on parameters
|
||||
const query: any = {}
|
||||
|
||||
// Authentication check for private operations (drafts or admin)
|
||||
let user = null
|
||||
try {
|
||||
user = await authMiddleware(request)
|
||||
} catch (error) {
|
||||
// Non-authenticated request - that's okay for public topic listing
|
||||
}
|
||||
|
||||
// If requesting user's own topics with drafts, verify authentication
|
||||
if (includeDrafts && !user) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: { message: 'Authentication required to view drafts', code: 'AUTH_REQUIRED' },
|
||||
},
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
// Filter by author if specified
|
||||
if (authorId) {
|
||||
query.authorId = authorId
|
||||
}
|
||||
|
||||
// Handle draft filtering
|
||||
if (fetchAll === 'true') {
|
||||
// Admin panel - show all posts (drafts and published)
|
||||
// No draft filtering applied
|
||||
} else if (includeDrafts && user && (authorId === user.id || !authorId)) {
|
||||
// User requesting their own topics (including drafts)
|
||||
if (!authorId) {
|
||||
query.authorId = user.id
|
||||
}
|
||||
} else {
|
||||
// Public topic listing - only published topics
|
||||
query.isDraft = false
|
||||
}
|
||||
|
||||
// Search functionality
|
||||
if (search) {
|
||||
query.$or = [
|
||||
{ title: { $regex: search, $options: 'i' } },
|
||||
{ excerpt: { $regex: search, $options: 'i' } },
|
||||
{ content: { $regex: search, $options: 'i' } },
|
||||
]
|
||||
}
|
||||
|
||||
// Filter by tag
|
||||
if (tag) {
|
||||
query['tags.name'] = { $regex: new RegExp(tag, 'i') }
|
||||
}
|
||||
|
||||
// Get total count with same filters
|
||||
const totalTopics = await TopicModel.countDocuments(query)
|
||||
|
||||
// Get paginated topics sorted by publishedAt
|
||||
const topics = await TopicModel.find(query)
|
||||
.sort({ publishedAt: -1 })
|
||||
.skip(skip)
|
||||
.limit(limit)
|
||||
.lean()
|
||||
|
||||
if (!topics || topics.length === 0) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
data: [],
|
||||
pagination: {
|
||||
total: 0,
|
||||
page,
|
||||
limit,
|
||||
totalPages: 0,
|
||||
},
|
||||
},
|
||||
{ status: 200 }
|
||||
)
|
||||
}
|
||||
|
||||
// Transform the topics using our utility function
|
||||
const transformedTopics = transformToTopics(topics)
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
data: transformedTopics,
|
||||
pagination: {
|
||||
total: totalTopics,
|
||||
page,
|
||||
limit,
|
||||
totalPages: Math.ceil(totalTopics / limit),
|
||||
},
|
||||
},
|
||||
{ status: 200 }
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('Error fetching topics:', error)
|
||||
|
||||
if (error instanceof ZodError) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Data validation failed',
|
||||
code: 'VALIDATION_ERROR',
|
||||
details: error.issues,
|
||||
},
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: { message: 'Failed to fetch topics', code: 'SERVER_ERROR' },
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
39
app/api/topics/tags/route.ts
Normal file
39
app/api/topics/tags/route.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import connectDB from '@/lib/mongodb'
|
||||
import TopicModel from '@/models/topic'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
await connectDB()
|
||||
|
||||
// Get all unique tags from published topics
|
||||
const topics = await TopicModel.find({ isDraft: false }).select('tags').lean()
|
||||
const tags = new Set<string>()
|
||||
|
||||
topics.forEach((topic) => {
|
||||
topic.tags.forEach((tag: { id?: string; name: string }) => tags.add(tag.name))
|
||||
})
|
||||
|
||||
const uniqueTags = Array.from(tags).map((name, index) => ({
|
||||
id: String(index + 1),
|
||||
name
|
||||
}))
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: uniqueTags,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error fetching tags:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Failed to fetch tags',
|
||||
code: 'TAGS_FETCH_ERROR',
|
||||
},
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
147
app/api/transactions/route.ts
Normal file
147
app/api/transactions/route.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { authMiddleware } from '@/lib/auth-middleware'
|
||||
import connectDB from '@/lib/mongodb'
|
||||
import { Transaction } from '@/models/transaction'
|
||||
import { Billing } from '@/models/billing'
|
||||
|
||||
// Query parameters schema
|
||||
const TransactionQuerySchema = z.object({
|
||||
page: z.string().optional().default('1'),
|
||||
limit: z.string().optional().default('10'),
|
||||
type: z.enum(['debit', 'credit', 'all']).optional().default('all'),
|
||||
service: z.string().optional(),
|
||||
})
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
// Authenticate user
|
||||
const user = await authMiddleware(request)
|
||||
if (!user) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: { message: 'Authentication required', code: 'UNAUTHORIZED' },
|
||||
},
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
await connectDB()
|
||||
|
||||
// Parse query parameters
|
||||
const { searchParams } = new URL(request.url)
|
||||
const queryParams = TransactionQuerySchema.parse({
|
||||
page: searchParams.get('page') || undefined,
|
||||
limit: searchParams.get('limit') || undefined,
|
||||
type: searchParams.get('type') || undefined,
|
||||
service: searchParams.get('service') || undefined,
|
||||
})
|
||||
|
||||
const page = parseInt(queryParams.page)
|
||||
const limit = Math.min(parseInt(queryParams.limit), 100) // Max 100 per page
|
||||
const skip = (page - 1) * limit
|
||||
|
||||
// Build query filter
|
||||
const filter: any = { email: user.email }
|
||||
|
||||
if (queryParams.type !== 'all') {
|
||||
filter.type = queryParams.type
|
||||
}
|
||||
|
||||
if (queryParams.service) {
|
||||
filter.service = { $regex: queryParams.service, $options: 'i' }
|
||||
}
|
||||
|
||||
// Build billing filter
|
||||
const billingFilter: any = {
|
||||
$or: [{ user: user.email }, { siliconId: user.id }],
|
||||
}
|
||||
// console.log('billingFilter', user.email)
|
||||
if (queryParams.service) {
|
||||
billingFilter.service = { $regex: queryParams.service, $options: 'i' }
|
||||
}
|
||||
|
||||
// Get both transactions and billing records
|
||||
const [transactions, billingRecords, transactionCount, billingCount] = await Promise.all([
|
||||
Transaction.find(filter).sort({ createdAt: -1 }).lean().exec(),
|
||||
Billing.find(billingFilter).sort({ createdAt: -1 }).lean().exec(),
|
||||
Transaction.countDocuments(filter).exec(),
|
||||
Billing.countDocuments(billingFilter).exec(),
|
||||
])
|
||||
|
||||
// Transform billing records to transaction format
|
||||
const transformedBillings = billingRecords.map((billing: any) => ({
|
||||
_id: billing._id,
|
||||
transactionId: billing.billing_id,
|
||||
type: 'debit' as const,
|
||||
amount: billing.amount,
|
||||
service: billing.service,
|
||||
serviceId: billing.serviceId,
|
||||
description: billing.remarks || `${billing.service} - ${billing.cycle} billing`,
|
||||
status: billing.status === 'pending' ? 'pending' : 'completed',
|
||||
previousBalance: 0, // We don't have this data in billing
|
||||
newBalance: 0, // We don't have this data in billing
|
||||
createdAt: billing.createdAt,
|
||||
updatedAt: billing.updatedAt,
|
||||
email: billing.user,
|
||||
userId: billing.siliconId,
|
||||
}))
|
||||
|
||||
// Combine and sort all records
|
||||
const allRecords = [...transactions, ...transformedBillings].sort(
|
||||
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
||||
)
|
||||
|
||||
// Apply type filter to combined records
|
||||
const filteredRecords =
|
||||
queryParams.type === 'all'
|
||||
? allRecords
|
||||
: allRecords.filter((record) => record.type === queryParams.type)
|
||||
|
||||
// Apply pagination to filtered records
|
||||
const totalCount = filteredRecords.length
|
||||
const paginatedRecords = filteredRecords.slice(skip, skip + limit)
|
||||
|
||||
const totalPages = Math.ceil(totalCount / limit)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
transactions: paginatedRecords,
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
totalCount,
|
||||
totalPages,
|
||||
hasNext: page < totalPages,
|
||||
hasPrev: page > 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Get transactions error:', error)
|
||||
|
||||
if (error instanceof z.ZodError) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Invalid query parameters',
|
||||
code: 'VALIDATION_ERROR',
|
||||
details: error.format(),
|
||||
},
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: { message: 'Failed to fetch transactions', code: 'INTERNAL_ERROR' },
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
102
app/api/upload/confirm/route.ts
Normal file
102
app/api/upload/confirm/route.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { authMiddleware } from '@/lib/auth-middleware'
|
||||
import { moveToPermStorage, generateUniqueFilename, deleteFile } from '@/lib/file-vault'
|
||||
import { z } from 'zod'
|
||||
|
||||
// Confirm upload request validation
|
||||
const confirmSchema = z.object({
|
||||
tempPath: z.string().min(1, 'Temporary path is required'),
|
||||
permanentFolder: z.string().optional().default('uploads'),
|
||||
filename: z.string().optional(),
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
// Check authentication
|
||||
const user = await authMiddleware(request)
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const { tempPath, permanentFolder, filename } = confirmSchema.parse(body)
|
||||
|
||||
// Validate temp path format
|
||||
if (!tempPath.startsWith('temp/')) {
|
||||
return NextResponse.json({ error: 'Invalid temporary path' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Generate permanent path
|
||||
const originalFilename = tempPath.split('/').pop() || 'file'
|
||||
const finalFilename = filename ? generateUniqueFilename(filename) : originalFilename
|
||||
const permanentPath = `${permanentFolder}/${finalFilename}`
|
||||
|
||||
// Move file from temp to permanent storage
|
||||
await moveToPermStorage(tempPath, permanentPath)
|
||||
|
||||
// TODO: Save file metadata to database
|
||||
// This would include:
|
||||
// - permanentPath
|
||||
// - originalFilename
|
||||
// - uploadedBy (user.id)
|
||||
// - uploadedAt
|
||||
// - fileSize
|
||||
// - mimeType
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
permanentPath,
|
||||
filename: finalFilename,
|
||||
folder: permanentFolder,
|
||||
confirmedBy: user.id,
|
||||
confirmedAt: new Date().toISOString(),
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Upload confirmation error:', error)
|
||||
|
||||
if (error instanceof z.ZodError) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid request parameters', details: error.issues },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
// Delete temporary file (cleanup)
|
||||
export async function DELETE(request: NextRequest) {
|
||||
try {
|
||||
// Check authentication
|
||||
const user = await authMiddleware(request)
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url)
|
||||
const tempPath = searchParams.get('path')
|
||||
|
||||
if (!tempPath) {
|
||||
return NextResponse.json({ error: 'Temporary path is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Validate temp path format
|
||||
if (!tempPath.startsWith('temp/')) {
|
||||
return NextResponse.json({ error: 'Invalid temporary path' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Delete temporary file
|
||||
await deleteFile(tempPath)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Temporary file deleted successfully',
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Temporary file deletion error:', error)
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
129
app/api/upload/route.ts
Normal file
129
app/api/upload/route.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { authMiddleware } from '@/lib/auth-middleware'
|
||||
import { uploadFile, validateFileType, validateFileSize } from '@/lib/file-vault'
|
||||
import { z } from 'zod'
|
||||
|
||||
// Allowed file types and sizes
|
||||
const ALLOWED_IMAGE_TYPES = ['image/jpeg', 'image/png', 'image/webp', 'image/gif']
|
||||
|
||||
const ALLOWED_DOCUMENT_TYPES = [
|
||||
'application/pdf',
|
||||
'application/msword',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'text/plain',
|
||||
]
|
||||
|
||||
const MAX_FILE_SIZE = 10 * 1024 * 1024 // 10MB
|
||||
|
||||
// Upload request validation
|
||||
const uploadSchema = z.object({
|
||||
type: z.enum(['image', 'document']).optional().default('image'),
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
// Check authentication
|
||||
const user = await authMiddleware(request)
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
// Parse form data
|
||||
const formData = await request.formData()
|
||||
const file = formData.get('file') as File
|
||||
const typeParam = formData.get('type') as string
|
||||
|
||||
if (!file) {
|
||||
return NextResponse.json({ error: 'No file provided' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Validate request parameters
|
||||
const { type } = uploadSchema.parse({ type: typeParam })
|
||||
|
||||
// Determine allowed file types based on upload type
|
||||
const allowedTypes = type === 'image' ? ALLOWED_IMAGE_TYPES : ALLOWED_DOCUMENT_TYPES
|
||||
|
||||
// Validate file type
|
||||
if (!validateFileType(file.type, allowedTypes)) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: `Invalid file type. Allowed types: ${allowedTypes.join(', ')}`,
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Validate file size
|
||||
if (!validateFileSize(file.size, MAX_FILE_SIZE)) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: `File size too large. Max size: ${MAX_FILE_SIZE / (1024 * 1024)}MB`,
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Convert file to buffer
|
||||
const bytes = await file.arrayBuffer()
|
||||
const buffer = Buffer.from(bytes)
|
||||
|
||||
// Upload file to external API
|
||||
const uploadResult = await uploadFile(buffer, file.name, file.type, user.id)
|
||||
|
||||
// Return upload information
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
tempPath: uploadResult.url, // Use URL as tempPath for compatibility
|
||||
filename: uploadResult.filename,
|
||||
size: file.size,
|
||||
type: file.type,
|
||||
uploadedBy: user.id,
|
||||
uploadedAt: new Date().toISOString(),
|
||||
url: uploadResult.url, // Also include the direct URL
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Upload error:', error)
|
||||
|
||||
if (error instanceof z.ZodError) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid request parameters', details: error.issues },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
// Get file information
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
// Check authentication
|
||||
const user = await authMiddleware(request)
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url)
|
||||
const filePath = searchParams.get('path')
|
||||
|
||||
if (!filePath) {
|
||||
return NextResponse.json({ error: 'File path is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
// TODO: Add file metadata retrieval from database
|
||||
// For now, return basic info
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
path: filePath,
|
||||
// Additional metadata would be retrieved from database
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('File retrieval error:', error)
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
79
app/api/user/balance/route.ts
Normal file
79
app/api/user/balance/route.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { authMiddleware } from '@/lib/auth-middleware'
|
||||
import connectDB from '@/lib/mongodb'
|
||||
import { User as UserModel } from '@/models/user'
|
||||
|
||||
// Schema for balance response
|
||||
const BalanceResponseSchema = z.object({
|
||||
success: z.boolean(),
|
||||
data: z.object({
|
||||
balance: z.number(),
|
||||
currency: z.string().default('INR'),
|
||||
lastUpdated: z.string(),
|
||||
}),
|
||||
})
|
||||
|
||||
// Get user's current balance
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
// Authenticate user
|
||||
const user = await authMiddleware(request)
|
||||
if (!user) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: { message: 'Authentication required', code: 'UNAUTHORIZED' },
|
||||
},
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
await connectDB()
|
||||
|
||||
// Get user's current balance from database
|
||||
const userData = await UserModel.findOne({ email: user.email })
|
||||
if (!userData) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: { message: 'User not found', code: 'USER_NOT_FOUND' },
|
||||
},
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
const responseData = {
|
||||
success: true,
|
||||
data: {
|
||||
balance: userData.balance || 0,
|
||||
currency: 'INR',
|
||||
lastUpdated: userData.updatedAt?.toISOString() || new Date().toISOString(),
|
||||
},
|
||||
}
|
||||
|
||||
// Validate response format
|
||||
const validatedResponse = BalanceResponseSchema.parse(responseData)
|
||||
return NextResponse.json(validatedResponse, { status: 200 })
|
||||
} catch (error) {
|
||||
console.error('Balance API error:', error)
|
||||
|
||||
if (error instanceof z.ZodError) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: { message: 'Invalid response format', code: 'VALIDATION_ERROR' },
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: { message: 'Failed to fetch balance', code: 'INTERNAL_ERROR' },
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
217
app/auth/page.tsx
Normal file
217
app/auth/page.tsx
Normal file
@@ -0,0 +1,217 @@
|
||||
'use client'
|
||||
import { useState, useEffect, Suspense } from 'react'
|
||||
import { useSearchParams, useRouter } from 'next/navigation'
|
||||
import { LoginForm } from '@/components/auth/LoginForm'
|
||||
import { RegisterForm } from '@/components/auth/RegisterForm'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { useAuth } from '@/contexts/AuthContext'
|
||||
import { Header } from '@/components/header'
|
||||
import { Footer } from '@/components/footer'
|
||||
import Link from 'next/link'
|
||||
import { BookOpen, Search, Settings, User, Edit, Tag } from 'lucide-react'
|
||||
|
||||
// Main auth component with search params handling
|
||||
function AuthPageContent() {
|
||||
const searchParams = useSearchParams()
|
||||
const router = useRouter()
|
||||
const [mode, setMode] = useState<'login' | 'register'>('login')
|
||||
const [shouldRedirect, setShouldRedirect] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const modeParam = searchParams.get('mode')
|
||||
if (modeParam === 'register') {
|
||||
setMode('register')
|
||||
}
|
||||
|
||||
// Only redirect if there's a returnUrl (user was redirected here) or if they just logged in
|
||||
const returnUrl = searchParams.get('returnUrl')
|
||||
if (returnUrl) {
|
||||
setShouldRedirect(true)
|
||||
}
|
||||
}, [searchParams])
|
||||
const { user, logout, loading } = useAuth()
|
||||
|
||||
// Only redirect if shouldRedirect is true (not just because user exists)
|
||||
useEffect(() => {
|
||||
if (!loading && user && shouldRedirect) {
|
||||
const returnUrl = searchParams.get('returnUrl')
|
||||
if (returnUrl) {
|
||||
router.push(decodeURIComponent(returnUrl))
|
||||
} else {
|
||||
router.push('/dashboard')
|
||||
}
|
||||
}
|
||||
}, [user, loading, router, searchParams, shouldRedirect])
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<Header />
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto mb-4"></div>
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (user) {
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<Header />
|
||||
<div className="container mx-auto px-4 pt-24 pb-8">
|
||||
<div className="max-w-4xl mx-auto space-y-6">
|
||||
{/* Welcome Section */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<User className="w-5 h-5" />
|
||||
Welcome back, {user.name}!
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
You're logged in and ready to start creating amazing content
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Edit className="w-5 h-5" />
|
||||
Topic Management
|
||||
</CardTitle>
|
||||
<CardDescription>Create and manage your topics</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<Button asChild className="w-full justify-start">
|
||||
<Link href="/topics/new">
|
||||
<BookOpen className="w-4 h-4 mr-2" />
|
||||
Create New Topic
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild variant="outline" className="w-full justify-start">
|
||||
<Link href="/topics">
|
||||
<Search className="w-4 h-4 mr-2" />
|
||||
View All Topics
|
||||
</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Settings className="w-5 h-5" />
|
||||
Account Settings
|
||||
</CardTitle>
|
||||
<CardDescription>Manage your account and preferences</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<Button asChild variant="outline" className="w-full justify-start">
|
||||
<Link href="/profile">
|
||||
<User className="w-4 h-4 mr-2" />
|
||||
Edit Profile
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild variant="outline" className="w-full justify-start">
|
||||
<Link href="/dashboard">
|
||||
<Settings className="w-4 h-4 mr-2" />
|
||||
Dashboard
|
||||
</Link>
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => logout()}
|
||||
variant="destructive"
|
||||
className="w-full justify-start"
|
||||
>
|
||||
Sign Out
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* User Info */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Account Information</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<strong>Email:</strong> {user.email}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Role:</strong> {user.role}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Provider:</strong> {user.provider}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Verified:</strong> {user.isVerified ? 'Yes' : 'No'}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<Header />
|
||||
<div className="container mx-auto px-4 pt-24 pb-8">
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-3xl font-bold mb-4">Authentication Demo</h1>
|
||||
<p className="text-muted-foreground mb-6">
|
||||
Test the authentication system with login and registration
|
||||
</p>
|
||||
<div className="flex justify-center gap-2 mb-6">
|
||||
<Button
|
||||
variant={mode === 'login' ? 'default' : 'outline'}
|
||||
onClick={() => setMode('login')}
|
||||
>
|
||||
Sign In
|
||||
</Button>
|
||||
<Button
|
||||
variant={mode === 'register' ? 'default' : 'outline'}
|
||||
onClick={() => setMode('register')}
|
||||
>
|
||||
Create Account
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{mode === 'login' ? (
|
||||
<LoginForm onSwitchToRegister={() => setMode('register')} />
|
||||
) : (
|
||||
<RegisterForm onSwitchToLogin={() => setMode('login')} />
|
||||
)}
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Wrapper component with Suspense boundary
|
||||
export default function AuthPage() {
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-primary"></div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<AuthPageContent />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
107
app/balance/page.tsx
Normal file
107
app/balance/page.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
import { useAuth } from '@/contexts/AuthContext'
|
||||
import { BalanceCard, BalanceDisplay, TransactionHistory } from '@/components/balance'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { ArrowLeft, History } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
|
||||
export default function BalancePage() {
|
||||
const { user } = useAuth()
|
||||
|
||||
if (!user) {
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="text-center">
|
||||
<h1 className="text-2xl font-bold mb-4">Access Denied</h1>
|
||||
<p className="text-muted-foreground mb-4">Please log in to view your balance.</p>
|
||||
<Link href="/auth">
|
||||
<Button>Login</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8 max-w-4xl">
|
||||
<div className="mb-6">
|
||||
<Link href="/">
|
||||
<Button variant="ghost" size="sm" className="mb-4">
|
||||
<ArrowLeft className="h-4 w-4 mr-2" />
|
||||
Back to Dashboard
|
||||
</Button>
|
||||
</Link>
|
||||
<h1 className="text-3xl font-bold">Balance Management</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Manage your account balance and view transaction history
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
{/* Main Balance Card */}
|
||||
<div className="space-y-6">
|
||||
<BalanceCard showAddBalance={true} />
|
||||
|
||||
{/* Quick Balance Display Examples */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Balance Display Variants</CardTitle>
|
||||
<CardDescription>Different ways to show balance in your UI</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div>
|
||||
<p className="text-sm font-medium mb-2">Default Display:</p>
|
||||
<BalanceDisplay variant="default" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-sm font-medium mb-2">Compact Display:</p>
|
||||
<BalanceDisplay variant="compact" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-sm font-medium mb-2">Badge Display:</p>
|
||||
<BalanceDisplay variant="badge" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Transaction History */}
|
||||
<div className="space-y-6">
|
||||
<TransactionHistory showFilters={true} maxHeight="500px" />
|
||||
|
||||
{/* Account Info */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Account Information</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Name:</span>
|
||||
<span className="font-medium">{user.name}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Email:</span>
|
||||
<span className="font-medium">{user.email}</span>
|
||||
</div>
|
||||
{user.siliconId && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Silicon ID:</span>
|
||||
<span className="font-medium">{user.siliconId}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Account Type:</span>
|
||||
<span className="font-medium capitalize">{user.role}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
51
app/billing/page.tsx
Normal file
51
app/billing/page.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import BillingHistory from '@/components/billing/BillingHistory'
|
||||
import { BalanceCard } from '@/components/balance/BalanceCard'
|
||||
|
||||
export default function BillingPage() {
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold mb-2">Billing & Usage</h1>
|
||||
<p className="text-gray-600">
|
||||
Manage your billing, view service usage, and track your spending across all SiliconPin
|
||||
services.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="history" className="space-y-6">
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="history">Billing History</TabsTrigger>
|
||||
<TabsTrigger value="balance">Balance Management</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="history" className="space-y-6">
|
||||
<BillingHistory />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="balance" className="space-y-6">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<BalanceCard />
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Payment Methods</CardTitle>
|
||||
<CardDescription>
|
||||
Manage your payment methods and billing preferences
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
Payment methods management coming soon
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
316
app/contact/contact-client.tsx
Normal file
316
app/contact/contact-client.tsx
Normal file
@@ -0,0 +1,316 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { Header } from '@/components/header'
|
||||
import { Footer } from '@/components/footer'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import { Mail, Phone, MapPin, Clock, Send, CheckCircle, AlertCircle } from 'lucide-react'
|
||||
|
||||
export function ContactPageClient() {
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
email: '',
|
||||
company: '',
|
||||
service_intrest: '',
|
||||
message: ''
|
||||
})
|
||||
const [errors, setErrors] = useState<Record<string, string>>({})
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [isSuccess, setIsSuccess] = useState(false)
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setIsSubmitting(true)
|
||||
setErrors({})
|
||||
|
||||
// Client-side validation
|
||||
const newErrors: Record<string, string> = {}
|
||||
if (!formData.name.trim()) newErrors.name = 'Name is required'
|
||||
if (!formData.email.trim()) newErrors.email = 'Email is required'
|
||||
if (!formData.service_intrest) newErrors.service_intrest = 'Service interest is required'
|
||||
if (!formData.message.trim()) newErrors.message = 'Message is required'
|
||||
|
||||
if (Object.keys(newErrors).length > 0) {
|
||||
setErrors(newErrors)
|
||||
setIsSubmitting(false)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/contact', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(formData),
|
||||
})
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
if (response.ok && result.success) {
|
||||
setIsSuccess(true)
|
||||
setFormData({
|
||||
name: '',
|
||||
email: '',
|
||||
company: '',
|
||||
service_intrest: '',
|
||||
message: ''
|
||||
})
|
||||
} else {
|
||||
if (result.errors) {
|
||||
setErrors(result.errors)
|
||||
} else {
|
||||
setErrors({ submit: result.message || 'Error submitting form. Please try again.' })
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
setErrors({ submit: 'Network error. Please try again.' })
|
||||
} finally {
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleInputChange = (field: string, value: string) => {
|
||||
setFormData(prev => ({ ...prev, [field]: value }))
|
||||
if (errors[field]) {
|
||||
setErrors(prev => ({ ...prev, [field]: '' }))
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<Header />
|
||||
<main className="container max-w-6xl pt-24 pb-8">
|
||||
{/* Page Header */}
|
||||
<div className="text-center mb-12">
|
||||
<h1 className="text-4xl font-bold tracking-tight mb-4">Contact Us</h1>
|
||||
<p className="text-xl text-muted-foreground max-w-3xl mx-auto">
|
||||
Have questions about our hosting services? Get in touch with our team of experts.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid lg:grid-cols-3 gap-8">
|
||||
{/* Contact Form */}
|
||||
<div className="lg:col-span-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl flex items-center">
|
||||
<Send className="w-6 h-6 mr-2 text-blue-600" />
|
||||
Send us a message
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{isSuccess && (
|
||||
<Alert className="mb-6 border-green-200 bg-green-50 dark:bg-green-950/10">
|
||||
<CheckCircle className="h-4 w-4 text-green-600" />
|
||||
<AlertDescription className="text-green-800 dark:text-green-200">
|
||||
Thank you for your message! We'll get back to you soon.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{errors.submit && (
|
||||
<Alert className="mb-6 border-red-200 bg-red-50 dark:bg-red-950/10">
|
||||
<AlertCircle className="h-4 w-4 text-red-600" />
|
||||
<AlertDescription className="text-red-800 dark:text-red-200">
|
||||
{errors.submit}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="name">Name*</Label>
|
||||
<Input
|
||||
id="name"
|
||||
type="text"
|
||||
placeholder="Your name"
|
||||
value={formData.name}
|
||||
onChange={(e) => handleInputChange('name', e.target.value)}
|
||||
className={errors.name ? 'border-red-500' : ''}
|
||||
/>
|
||||
{errors.name && (
|
||||
<p className="text-sm text-red-600">{errors.name}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email*</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="your.email@example.com"
|
||||
value={formData.email}
|
||||
onChange={(e) => handleInputChange('email', e.target.value)}
|
||||
className={errors.email ? 'border-red-500' : ''}
|
||||
/>
|
||||
{errors.email && (
|
||||
<p className="text-sm text-red-600">{errors.email}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="company">Company</Label>
|
||||
<Input
|
||||
id="company"
|
||||
type="text"
|
||||
placeholder="Your company name"
|
||||
value={formData.company}
|
||||
onChange={(e) => handleInputChange('company', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="service_intrest">Service Interest*</Label>
|
||||
<Select
|
||||
value={formData.service_intrest}
|
||||
onValueChange={(value) => handleInputChange('service_intrest', value)}
|
||||
>
|
||||
<SelectTrigger className={errors.service_intrest ? 'border-red-500' : ''}>
|
||||
<SelectValue placeholder="Select a service" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="Web Hosting">Web Hosting</SelectItem>
|
||||
<SelectItem value="VPS Hosting">VPS Hosting</SelectItem>
|
||||
<SelectItem value="Dedicated Server">Dedicated Server</SelectItem>
|
||||
<SelectItem value="Cloud Instance">Cloud Instance</SelectItem>
|
||||
<SelectItem value="Kubernetes">Kubernetes</SelectItem>
|
||||
<SelectItem value="Control Panel">Control Panel</SelectItem>
|
||||
<SelectItem value="VPN Services">VPN Services</SelectItem>
|
||||
<SelectItem value="Human Developer">Human Developer</SelectItem>
|
||||
<SelectItem value="Others">Others</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{errors.service_intrest && (
|
||||
<p className="text-sm text-red-600">{errors.service_intrest}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="message">Message*</Label>
|
||||
<Textarea
|
||||
id="message"
|
||||
placeholder="Tell us about your project requirements..."
|
||||
rows={5}
|
||||
value={formData.message}
|
||||
onChange={(e) => handleInputChange('message', e.target.value)}
|
||||
className={errors.message ? 'border-red-500' : ''}
|
||||
/>
|
||||
{errors.message && (
|
||||
<p className="text-sm text-red-600">{errors.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full md:w-auto"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin mr-2" />
|
||||
Sending...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Send className="w-4 h-4 mr-2" />
|
||||
Send Message
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Contact Information */}
|
||||
<div className="space-y-6">
|
||||
{/* Our Information */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">Our Information</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-start space-x-3">
|
||||
<Mail className="w-5 h-5 text-blue-600 mt-0.5" />
|
||||
<div>
|
||||
<p className="font-medium">Email</p>
|
||||
<p className="text-sm text-muted-foreground">contact@siliconpin.com</p>
|
||||
<p className="text-sm text-muted-foreground">support@siliconpin.com</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start space-x-3">
|
||||
<Phone className="w-5 h-5 text-green-600 mt-0.5" />
|
||||
<div>
|
||||
<p className="font-medium">Phone</p>
|
||||
<p className="text-sm text-muted-foreground">+91-700-160-1485</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start space-x-3">
|
||||
<MapPin className="w-5 h-5 text-red-600 mt-0.5" />
|
||||
<div>
|
||||
<p className="font-medium">Address</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
121 Lalbari, GourBongo Road<br />
|
||||
Habra, West Bengal 743271<br />
|
||||
India
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Business Hours */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl flex items-center">
|
||||
<Clock className="w-5 h-5 mr-2 text-purple-600" />
|
||||
Business Hours
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm font-medium">Monday:</span>
|
||||
<span className="text-sm text-muted-foreground">Closed</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm font-medium">Tuesday - Friday:</span>
|
||||
<span className="text-sm text-muted-foreground">9:00 AM - 5:00 PM</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm font-medium">Saturday:</span>
|
||||
<span className="text-sm text-muted-foreground">10:00 AM - 4:00 PM</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm font-medium">Sunday:</span>
|
||||
<span className="text-sm text-muted-foreground">Closed</span>
|
||||
</div>
|
||||
<div className="border-t pt-3 mt-3">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm font-medium text-green-600">Technical Support:</span>
|
||||
<span className="text-sm font-medium text-green-600">24/7</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
12
app/contact/page.tsx
Normal file
12
app/contact/page.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Metadata } from 'next'
|
||||
import { generateMetadata } from '@/lib/seo'
|
||||
import { ContactPageClient } from './contact-client'
|
||||
|
||||
export const metadata: Metadata = generateMetadata({
|
||||
title: 'Contact Us - SiliconPin',
|
||||
description: 'Have questions about our hosting services? Get in touch with our team of experts.',
|
||||
})
|
||||
|
||||
export default function ContactPage() {
|
||||
return <ContactPageClient />
|
||||
}
|
||||
462
app/dashboard/page.tsx
Normal file
462
app/dashboard/page.tsx
Normal file
@@ -0,0 +1,462 @@
|
||||
'use client'
|
||||
import { RequireAuth } from '@/components/auth/RequireAuth'
|
||||
import { useAuth } from '@/contexts/AuthContext'
|
||||
import { Header } from '@/components/header'
|
||||
import { Footer } from '@/components/footer'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import Link from 'next/link'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useState, useMemo } from 'react'
|
||||
import {
|
||||
BookOpen,
|
||||
Edit3,
|
||||
Eye,
|
||||
Heart,
|
||||
TrendingUp,
|
||||
Clock,
|
||||
Users,
|
||||
Plus,
|
||||
RefreshCw,
|
||||
Trash2
|
||||
} from 'lucide-react'
|
||||
|
||||
// Types matching the API response from /api/dashboard
|
||||
interface DashboardStats {
|
||||
totalTopics: number
|
||||
publishedTopics: number
|
||||
draftTopics: number
|
||||
totalViews: number
|
||||
}
|
||||
|
||||
interface UserTopic {
|
||||
id: string
|
||||
title: string
|
||||
slug: string
|
||||
publishedAt: number
|
||||
isDraft: boolean
|
||||
views?: number
|
||||
excerpt: string
|
||||
tags: { name: string }[]
|
||||
}
|
||||
|
||||
interface DashboardResponse {
|
||||
success: boolean
|
||||
data: {
|
||||
stats: DashboardStats
|
||||
userTopics: UserTopic[]
|
||||
}
|
||||
}
|
||||
|
||||
type FilterType = 'all' | 'published' | 'drafts'
|
||||
|
||||
// Fetch dashboard data from the API
|
||||
const fetchDashboardData = async (): Promise<DashboardResponse> => {
|
||||
const response = await fetch('/api/dashboard', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
credentials: 'include', // Include cookies for authentication
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ error: { message: 'Unknown error' } }))
|
||||
throw new Error(errorData.error?.message || `HTTP ${response.status}: Failed to fetch dashboard data`)
|
||||
}
|
||||
|
||||
return response.json()
|
||||
}
|
||||
|
||||
function DashboardContent() {
|
||||
const { user } = useAuth()
|
||||
const [filterType, setFilterType] = useState<FilterType>('all')
|
||||
const [deletingTopic, setDeletingTopic] = useState<string | null>(null)
|
||||
|
||||
// Use TanStack Query for data fetching with automatic caching and refetching
|
||||
const {
|
||||
data: dashboardData,
|
||||
isLoading,
|
||||
error,
|
||||
refetch,
|
||||
isRefetching,
|
||||
} = useQuery({
|
||||
queryKey: ['dashboard', user?.id],
|
||||
queryFn: fetchDashboardData,
|
||||
enabled: !!user, // Only fetch when user is authenticated
|
||||
staleTime: 0, // Always consider data stale - fetch fresh data
|
||||
gcTime: 0, // Don't cache data
|
||||
retry: (failureCount, error: any) => {
|
||||
// Don't retry on authentication errors
|
||||
if (error.message.includes('401') || error.message.includes('Unauthorized')) {
|
||||
return false
|
||||
}
|
||||
return failureCount < 3
|
||||
},
|
||||
})
|
||||
|
||||
// Extract data from the response
|
||||
const stats = dashboardData?.data?.stats
|
||||
const userTopics = dashboardData?.data?.userTopics || []
|
||||
|
||||
// Filter topics based on the selected filter type
|
||||
const filteredTopics = useMemo(() => {
|
||||
switch (filterType) {
|
||||
case 'published':
|
||||
return userTopics.filter(topic => !topic.isDraft)
|
||||
case 'drafts':
|
||||
return userTopics.filter(topic => topic.isDraft)
|
||||
default:
|
||||
return userTopics
|
||||
}
|
||||
}, [userTopics, filterType])
|
||||
|
||||
// Handle topic deletion
|
||||
const handleDeleteTopic = async (topicId: string, topicTitle: string) => {
|
||||
if (!confirm(`Are you sure you want to delete "${topicTitle}"? This action cannot be undone.`)) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setDeletingTopic(topicId)
|
||||
|
||||
const response = await fetch(`/api/topic?id=${topicId}`, {
|
||||
method: 'DELETE',
|
||||
credentials: 'include',
|
||||
})
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
throw new Error(result.error?.message || 'Failed to delete topic')
|
||||
}
|
||||
|
||||
// Refetch dashboard data to update the UI
|
||||
refetch()
|
||||
|
||||
// Optional: Show success message
|
||||
// You can add a toast notification here if you have a toast system
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to delete topic:', error)
|
||||
alert(`Failed to delete topic: ${error instanceof Error ? error.message : 'Unknown error'}`)
|
||||
} finally {
|
||||
setDeletingTopic(null)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle loading state
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="container pt-24 pb-8">
|
||||
<div className="max-w-7xl mx-auto space-y-8">
|
||||
<div>
|
||||
<Skeleton className="h-8 w-64 mb-2" />
|
||||
<Skeleton className="h-4 w-96" />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{Array.from({ length: 4 }).map((_, i) => (
|
||||
<Card key={i}>
|
||||
<CardContent className="p-6">
|
||||
<Skeleton className="h-8 w-8 mb-4" />
|
||||
<Skeleton className="h-6 w-16 mb-2" />
|
||||
<Skeleton className="h-4 w-24" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Skeleton className="h-6 w-32" />
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{Array.from({ length: 3 }).map((_, i) => (
|
||||
<div key={i} className="flex justify-between items-center">
|
||||
<div>
|
||||
<Skeleton className="h-4 w-48 mb-1" />
|
||||
<Skeleton className="h-3 w-24" />
|
||||
</div>
|
||||
<Skeleton className="h-6 w-12" />
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Skeleton className="h-6 w-24" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Skeleton className="h-10 w-full mb-4" />
|
||||
<Skeleton className="h-4 w-full mb-2" />
|
||||
<Skeleton className="h-4 w-3/4" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Handle error state
|
||||
if (error) {
|
||||
return (
|
||||
<div className="container pt-24 pb-8">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="flex flex-col items-center justify-center min-h-[400px] text-center">
|
||||
<div className="text-red-500 mb-4">
|
||||
<BookOpen className="w-12 h-12 mx-auto mb-2" />
|
||||
<h3 className="text-lg font-semibold">Failed to Load Dashboard</h3>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{error.message || 'An unexpected error occurred'}
|
||||
</p>
|
||||
</div>
|
||||
<Button onClick={() => refetch()} disabled={isRefetching}>
|
||||
{isRefetching ? (
|
||||
<RefreshCw className="w-4 h-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<RefreshCw className="w-4 h-4 mr-2" />
|
||||
)}
|
||||
Try Again
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container pt-24 pb-8">
|
||||
<div className="max-w-7xl mx-auto space-y-8">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">Dashboard</h1>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
Welcome back, {user?.name}! Here's your topics overview.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
onClick={() => refetch()}
|
||||
disabled={isRefetching}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
>
|
||||
{isRefetching ? (
|
||||
<RefreshCw className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
)}
|
||||
</Button>
|
||||
<Button asChild size="sm">
|
||||
<Link href="/topics/new">
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
New Topic
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center">
|
||||
<BookOpen className="h-8 w-8 text-blue-600" />
|
||||
<div className="ml-4">
|
||||
<p className="text-2xl font-bold">{stats?.totalTopics || 0}</p>
|
||||
<p className="text-xs text-muted-foreground">Total Topics</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center">
|
||||
<Edit3 className="h-8 w-8 text-green-600" />
|
||||
<div className="ml-4">
|
||||
<p className="text-2xl font-bold">{stats?.publishedTopics || 0}</p>
|
||||
<p className="text-xs text-muted-foreground">Published</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center">
|
||||
<Eye className="h-8 w-8 text-purple-600" />
|
||||
<div className="ml-4">
|
||||
<p className="text-2xl font-bold">{stats?.totalViews?.toLocaleString() || '0'}</p>
|
||||
<p className="text-xs text-muted-foreground">Total Views</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center">
|
||||
<Heart className="h-8 w-8 text-red-600" />
|
||||
<div className="ml-4">
|
||||
<p className="text-2xl font-bold">0</p>
|
||||
<p className="text-xs text-muted-foreground">Total Likes</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Content Grid */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
{/* Recent Topics with Filter */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Clock className="w-5 h-5" />
|
||||
Recent Topics
|
||||
</CardTitle>
|
||||
<CardDescription>Your latest topics</CardDescription>
|
||||
</div>
|
||||
<Select value={filterType} onValueChange={(value: FilterType) => setFilterType(value)}>
|
||||
<SelectTrigger className="w-32">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Topics</SelectItem>
|
||||
<SelectItem value="published">Published</SelectItem>
|
||||
<SelectItem value="drafts">Drafts</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{filteredTopics.length === 0 ? (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
<BookOpen className="w-8 h-8 mx-auto mb-2 opacity-50" />
|
||||
<p>
|
||||
{filterType === 'all'
|
||||
? 'No topics yet. Start creating your first topic!'
|
||||
: filterType === 'drafts'
|
||||
? 'No draft topics found.'
|
||||
: 'No published topics found.'}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3 max-h-96 overflow-y-auto pr-2">
|
||||
{filteredTopics.map((topic) => (
|
||||
<div key={topic.id} className="flex justify-between items-center p-3 border rounded-lg hover:bg-accent/50 transition-colors">
|
||||
<div className="flex-1 min-w-0">
|
||||
<h4 className="font-medium hover:text-primary transition-colors truncate">
|
||||
{topic.title}
|
||||
</h4>
|
||||
<div className="flex items-center gap-4 mt-1">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{new Date(topic.publishedAt).toLocaleDateString()}
|
||||
</p>
|
||||
<span className={`text-xs px-2 py-1 rounded ${
|
||||
topic.isDraft
|
||||
? 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200'
|
||||
: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'
|
||||
}`}>
|
||||
{topic.isDraft ? 'Draft' : 'Published'}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-1 line-clamp-1">
|
||||
{topic.excerpt}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 ml-4">
|
||||
{!topic.isDraft && (
|
||||
<div className="flex items-center text-sm text-muted-foreground">
|
||||
<Eye className="w-4 h-4 mr-1" />
|
||||
{topic.views || 0}
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
asChild
|
||||
variant="outline"
|
||||
size="sm"
|
||||
>
|
||||
<Link href={`/topics/edit/${topic.id}`}>
|
||||
<Edit3 className="w-4 h-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleDeleteTopic(topic.id, topic.title)}
|
||||
disabled={deletingTopic === topic.id}
|
||||
className="text-red-600 hover:text-red-700 hover:bg-red-50"
|
||||
>
|
||||
{deletingTopic === topic.id ? (
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-red-600"></div>
|
||||
) : (
|
||||
<Trash2 className="w-4 h-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Quick Actions - Cleaned up */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<TrendingUp className="w-5 h-5" />
|
||||
Quick Actions
|
||||
</CardTitle>
|
||||
<CardDescription>Get things done faster</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<Button asChild className="w-full justify-start h-auto p-4">
|
||||
<Link href="/topics/new">
|
||||
<Edit3 className="w-5 h-5 mr-3" />
|
||||
<div className="text-left">
|
||||
<div className="font-medium">Create New Topic</div>
|
||||
<div className="text-xs text-muted-foreground">Start creating fresh content</div>
|
||||
</div>
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
<Button asChild variant="outline" className="w-full justify-start h-auto p-4">
|
||||
<Link href="/profile">
|
||||
<Users className="w-5 h-5 mr-3" />
|
||||
<div className="text-left">
|
||||
<div className="font-medium">Profile Settings</div>
|
||||
<div className="text-xs text-muted-foreground">Update your information</div>
|
||||
</div>
|
||||
</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function DashboardPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<Header />
|
||||
<RequireAuth>
|
||||
<DashboardContent />
|
||||
</RequireAuth>
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
107
app/error.tsx
Normal file
107
app/error.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { Header } from '@/components/header'
|
||||
import { Footer } from '@/components/footer'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import { RefreshCcw, Home, AlertTriangle, Server } from 'lucide-react'
|
||||
|
||||
interface ErrorProps {
|
||||
error: Error & { digest?: string }
|
||||
reset: () => void
|
||||
}
|
||||
|
||||
export default function Error({ error, reset }: ErrorProps) {
|
||||
useEffect(() => {
|
||||
// Log the error to an error reporting service
|
||||
console.error('Application error:', error)
|
||||
}, [error])
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background flex flex-col">
|
||||
<Header />
|
||||
<main className="flex-1 flex items-center justify-center py-12">
|
||||
<div className="container max-w-2xl text-center">
|
||||
{/* Error Visual */}
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center justify-center space-x-4 mb-6">
|
||||
<div className="w-16 h-16 bg-red-100 dark:bg-red-950/20 rounded-lg flex items-center justify-center">
|
||||
<AlertTriangle className="w-8 h-8 text-red-600" />
|
||||
</div>
|
||||
<div className="text-6xl font-bold text-red-600">500</div>
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold tracking-tight mb-4">Something Went Wrong</h1>
|
||||
<p className="text-xl text-muted-foreground mb-8">
|
||||
We encountered an unexpected error. Our team has been notified and is working on a fix.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Error Details */}
|
||||
<Alert className="mb-6 border-red-200 bg-red-50 dark:bg-red-950/10 text-left">
|
||||
<AlertTriangle className="h-4 w-4 text-red-600" />
|
||||
<AlertDescription className="text-red-800 dark:text-red-200">
|
||||
<div className="space-y-2">
|
||||
<p><strong>Error:</strong> {error.message || 'An unexpected error occurred'}</p>
|
||||
{error.digest && (
|
||||
<p className="text-xs opacity-75"><strong>Error ID:</strong> {error.digest}</p>
|
||||
)}
|
||||
</div>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
{/* Recovery Actions */}
|
||||
<Card>
|
||||
<CardContent className="pt-6 space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<Button onClick={reset} className="h-12">
|
||||
<RefreshCcw className="w-4 h-4 mr-2" />
|
||||
Try Again
|
||||
</Button>
|
||||
|
||||
<Button asChild variant="outline" className="h-12">
|
||||
<Link href="/">
|
||||
<Home className="w-4 h-4 mr-2" />
|
||||
Go Home
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-muted-foreground">
|
||||
<p className="mb-2">If the problem persists, you can:</p>
|
||||
<div className="flex flex-wrap justify-center gap-4">
|
||||
<a href="/contact" className="text-primary hover:underline">Contact Support</a>
|
||||
<a href="/feedback" className="text-primary hover:underline">Report Issue</a>
|
||||
<a href="https://status.siliconpin.com" className="text-primary hover:underline" target="_blank" rel="noopener noreferrer">Check Status</a>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Technical Details */}
|
||||
<details className="mt-8 text-left">
|
||||
<summary className="cursor-pointer text-sm text-muted-foreground hover:text-foreground mb-2">
|
||||
Technical Details (for developers)
|
||||
</summary>
|
||||
<div className="p-4 bg-muted rounded-lg text-sm">
|
||||
<pre className="whitespace-pre-wrap break-words">
|
||||
<strong>Error Message:</strong> {error.message}
|
||||
{error.stack && (
|
||||
<>
|
||||
<br /><br />
|
||||
<strong>Stack Trace:</strong>
|
||||
<br />
|
||||
{error.stack}
|
||||
</>
|
||||
)}
|
||||
</pre>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
227
app/features/page.tsx
Normal file
227
app/features/page.tsx
Normal file
@@ -0,0 +1,227 @@
|
||||
import { Metadata } from 'next'
|
||||
import { generateMetadata } from '@/lib/seo'
|
||||
import { Header } from '@/components/header'
|
||||
import { Footer } from '@/components/footer'
|
||||
|
||||
export const metadata: Metadata = generateMetadata({
|
||||
title: 'Features - NextJS Boilerplate',
|
||||
description: 'Explore all the features and capabilities of the NextJS Boilerplate.',
|
||||
})
|
||||
|
||||
export default function FeaturesPage() {
|
||||
const features = [
|
||||
{
|
||||
category: 'Framework & Language',
|
||||
items: [
|
||||
{
|
||||
name: 'Next.js 15',
|
||||
description:
|
||||
'Latest Next.js with App Router, React Server Components, and improved performance',
|
||||
icon: '⚡',
|
||||
color: 'bg-blue-500',
|
||||
},
|
||||
{
|
||||
name: 'TypeScript',
|
||||
description: 'Full type safety with proper interfaces and strict type checking',
|
||||
icon: '🔷',
|
||||
color: 'bg-blue-600',
|
||||
},
|
||||
{
|
||||
name: 'React 19',
|
||||
description: 'Latest React with concurrent features and improved developer experience',
|
||||
icon: '⚛️',
|
||||
color: 'bg-cyan-500',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
category: 'Authentication & Security',
|
||||
items: [
|
||||
{
|
||||
name: 'JWT Authentication',
|
||||
description: 'Secure JWT-based authentication with refresh token support',
|
||||
icon: '🔐',
|
||||
color: 'bg-green-500',
|
||||
},
|
||||
{
|
||||
name: 'Redis Sessions',
|
||||
description: 'High-performance session storage using Redis for scalability',
|
||||
icon: '⚡',
|
||||
color: 'bg-red-500',
|
||||
},
|
||||
{
|
||||
name: 'Protected Routes',
|
||||
description: 'Middleware-based route protection with automatic redirects',
|
||||
icon: '🛡️',
|
||||
color: 'bg-orange-500',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
category: 'Database & Storage',
|
||||
items: [
|
||||
{
|
||||
name: 'MongoDB Integration',
|
||||
description: 'Complete MongoDB setup with Mongoose ODM and connection pooling',
|
||||
icon: '🗄️',
|
||||
color: 'bg-green-600',
|
||||
},
|
||||
{
|
||||
name: 'Zod Validation',
|
||||
description: 'Schema validation for APIs and forms with TypeScript inference',
|
||||
icon: '✅',
|
||||
color: 'bg-purple-500',
|
||||
},
|
||||
{
|
||||
name: 'File Upload System',
|
||||
description: 'Ready-to-use file upload with MinIO integration and validation',
|
||||
icon: '📁',
|
||||
color: 'bg-indigo-500',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
category: 'UI & Styling',
|
||||
items: [
|
||||
{
|
||||
name: 'Tailwind CSS',
|
||||
description: 'Utility-first CSS framework with custom design system',
|
||||
icon: '🎨',
|
||||
color: 'bg-cyan-600',
|
||||
},
|
||||
{
|
||||
name: 'Custom UI Components',
|
||||
description: 'High-quality, accessible React components with consistent design',
|
||||
icon: '🧩',
|
||||
color: 'bg-violet-500',
|
||||
},
|
||||
{
|
||||
name: 'Dark Mode Support',
|
||||
description: 'Complete dark/light theme system with smooth transitions',
|
||||
icon: '🌓',
|
||||
color: 'bg-gray-600',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
category: 'State Management',
|
||||
items: [
|
||||
{
|
||||
name: 'TanStack Query',
|
||||
description: 'Powerful server state management with caching and synchronization',
|
||||
icon: '🔄',
|
||||
color: 'bg-orange-600',
|
||||
},
|
||||
{
|
||||
name: 'React Context',
|
||||
description: 'Clean client state management for authentication and UI state',
|
||||
icon: '🎯',
|
||||
color: 'bg-pink-500',
|
||||
},
|
||||
{
|
||||
name: 'Form Handling',
|
||||
description: 'React Hook Form integration with validation and error handling',
|
||||
icon: '📝',
|
||||
color: 'bg-teal-500',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
category: 'Developer Experience',
|
||||
items: [
|
||||
{
|
||||
name: 'Code Quality Tools',
|
||||
description: 'ESLint, Prettier, and Husky pre-commit hooks for consistent code',
|
||||
icon: '🔧',
|
||||
color: 'bg-yellow-600',
|
||||
},
|
||||
{
|
||||
name: 'Development Templates',
|
||||
description: 'Ready-to-use templates for components, pages, API routes, and hooks',
|
||||
icon: '📋',
|
||||
color: 'bg-emerald-500',
|
||||
},
|
||||
{
|
||||
name: 'Docker Ready',
|
||||
description: 'Multi-stage Dockerfile and docker-compose for development and production',
|
||||
icon: '🐳',
|
||||
color: 'bg-blue-700',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<Header />
|
||||
<div className="container mx-auto px-4 pt-24 pb-16">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
{/* Page Header */}
|
||||
<div className="text-center mb-16">
|
||||
<h1 className="text-4xl md:text-5xl font-bold mb-6 bg-gradient-to-r from-blue-600 via-purple-600 to-indigo-600 bg-clip-text text-transparent">
|
||||
Features & Capabilities
|
||||
</h1>
|
||||
<p className="text-xl text-muted-foreground max-w-3xl mx-auto">
|
||||
Everything you need to build modern, scalable web applications with best practices
|
||||
built-in
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Features Grid */}
|
||||
<div className="space-y-16">
|
||||
{features.map((category, categoryIndex) => (
|
||||
<section key={categoryIndex}>
|
||||
<h2 className="text-2xl font-semibold mb-8 text-center">{category.category}</h2>
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{category.items.map((feature, featureIndex) => (
|
||||
<div
|
||||
key={featureIndex}
|
||||
className="group p-6 rounded-2xl bg-card border hover:shadow-lg transition-all duration-300 transform hover:-translate-y-1"
|
||||
>
|
||||
<div className="flex items-start space-x-4">
|
||||
<div
|
||||
className={`w-12 h-12 rounded-xl flex items-center justify-center text-white ${feature.color} group-hover:scale-110 transition-transform`}
|
||||
>
|
||||
<span className="text-xl">{feature.icon}</span>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="font-semibold text-lg mb-2 group-hover:text-primary transition-colors">
|
||||
{feature.name}
|
||||
</h3>
|
||||
<p className="text-muted-foreground text-sm leading-relaxed">
|
||||
{feature.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Call to Action */}
|
||||
<div className="mt-20 text-center bg-card rounded-2xl p-12 border">
|
||||
<h2 className="text-3xl font-bold mb-4">Ready to Start Building?</h2>
|
||||
<p className="text-muted-foreground mb-8 max-w-2xl mx-auto">
|
||||
All these features are ready to use out of the box. Clone the repository, customize it
|
||||
for your needs, and start building your next great application.
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-4 justify-center">
|
||||
<div className="px-6 py-3 bg-primary text-primary-foreground rounded-lg font-medium">
|
||||
Production Ready
|
||||
</div>
|
||||
<div className="px-6 py-3 bg-secondary text-secondary-foreground rounded-lg font-medium">
|
||||
Well Documented
|
||||
</div>
|
||||
<div className="px-6 py-3 bg-accent text-accent-foreground rounded-lg font-medium">
|
||||
Easily Customizable
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
322
app/feedback/feedback-client.tsx
Normal file
322
app/feedback/feedback-client.tsx
Normal file
@@ -0,0 +1,322 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { Header } from '@/components/header'
|
||||
import { Footer } from '@/components/footer'
|
||||
import { useAuth } from '@/contexts/AuthContext'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { MessageSquare, AlertTriangle, Send, CheckCircle, AlertCircle } from 'lucide-react'
|
||||
|
||||
export function FeedbackPageClient() {
|
||||
const { user } = useAuth()
|
||||
|
||||
const [formType, setFormType] = useState<'suggestion' | 'report'>('suggestion')
|
||||
const [formData, setFormData] = useState({
|
||||
name: user?.name || '',
|
||||
email: user?.email || '',
|
||||
title: '',
|
||||
details: '',
|
||||
category: 'general',
|
||||
urgency: 'low'
|
||||
})
|
||||
const [errors, setErrors] = useState<Record<string, string>>({})
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [isSuccess, setIsSuccess] = useState(false)
|
||||
|
||||
const categoryOptions = [
|
||||
{ value: 'general', label: 'General' },
|
||||
{ value: 'ui', label: 'User Interface' },
|
||||
{ value: 'performance', label: 'Performance' },
|
||||
{ value: 'feature', label: 'Feature Request' },
|
||||
{ value: 'technical', label: 'Technical Issue' },
|
||||
{ value: 'billing', label: 'Billing/Payment' },
|
||||
{ value: 'security', label: 'Security' },
|
||||
{ value: 'other', label: 'Other' }
|
||||
]
|
||||
|
||||
const urgencyOptions = [
|
||||
{ value: 'low', label: 'Low - Not urgent' },
|
||||
{ value: 'medium', label: 'Medium - Needs attention' },
|
||||
{ value: 'high', label: 'High - Impacts usage' },
|
||||
{ value: 'critical', label: 'Critical - Service unavailable' }
|
||||
]
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setIsSubmitting(true)
|
||||
setErrors({})
|
||||
|
||||
// Client-side validation
|
||||
const newErrors: Record<string, string> = {}
|
||||
if (!formData.name.trim()) newErrors.name = 'Please enter your name.'
|
||||
if (!formData.email.trim() || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
|
||||
newErrors.email = 'Please enter a valid email address.'
|
||||
}
|
||||
if (!formData.title.trim()) newErrors.title = `Please enter a title for your ${formType}.`
|
||||
if (!formData.details.trim()) newErrors.details = `Please provide details for your ${formType}.`
|
||||
|
||||
if (Object.keys(newErrors).length > 0) {
|
||||
setErrors(newErrors)
|
||||
setIsSubmitting(false)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/feedback', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
...formData,
|
||||
type: formType,
|
||||
siliconId: user?.id || null
|
||||
}),
|
||||
})
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
if (response.ok && result.success) {
|
||||
setIsSuccess(true)
|
||||
setFormData({
|
||||
name: user?.name || '',
|
||||
email: user?.email || '',
|
||||
title: '',
|
||||
details: '',
|
||||
category: 'general',
|
||||
urgency: 'low'
|
||||
})
|
||||
setFormType('suggestion')
|
||||
} else {
|
||||
if (result.errors) {
|
||||
setErrors(result.errors)
|
||||
} else {
|
||||
setErrors({ submit: result.message || 'Error submitting form. Please try again.' })
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
setErrors({ submit: 'Network error. Please try again.' })
|
||||
} finally {
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleInputChange = (field: string, value: string) => {
|
||||
setFormData(prev => ({ ...prev, [field]: value }))
|
||||
if (errors[field]) {
|
||||
setErrors(prev => ({ ...prev, [field]: '' }))
|
||||
}
|
||||
}
|
||||
|
||||
const handleTypeToggle = (checked: boolean) => {
|
||||
setFormType(checked ? 'report' : 'suggestion')
|
||||
setIsSuccess(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<Header />
|
||||
<main className="container max-w-4xl pt-24 pb-8">
|
||||
{/* Page Header */}
|
||||
<div className="text-center mb-12">
|
||||
<h1 className="text-4xl font-bold tracking-tight mb-4">Suggestion or Report</h1>
|
||||
<p className="text-xl text-muted-foreground max-w-3xl mx-auto">
|
||||
We value your feedback! Use this form to submit suggestions or report any issues you've encountered.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
{/* Success Message */}
|
||||
{isSuccess && (
|
||||
<Alert className="mb-6 border-green-200 bg-green-50 dark:bg-green-950/10">
|
||||
<CheckCircle className="h-4 w-4 text-green-600" />
|
||||
<AlertDescription className="text-green-800 dark:text-green-200">
|
||||
{formType === 'suggestion'
|
||||
? 'Thank you for your suggestion! We appreciate your feedback.'
|
||||
: 'Your report has been submitted. We will look into it as soon as possible.'
|
||||
}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Error Message */}
|
||||
{errors.submit && (
|
||||
<Alert className="mb-6 border-red-200 bg-red-50 dark:bg-red-950/10">
|
||||
<AlertCircle className="h-4 w-4 text-red-600" />
|
||||
<AlertDescription className="text-red-800 dark:text-red-200">
|
||||
{errors.submit}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{/* Form Type Toggle */}
|
||||
<div className="flex items-center justify-center space-x-4 p-4 bg-muted rounded-lg">
|
||||
<div className={`flex items-center space-x-2 ${formType === 'suggestion' ? 'text-primary font-medium' : 'text-muted-foreground'}`}>
|
||||
<MessageSquare className="w-4 h-4" />
|
||||
<span>Suggestion</span>
|
||||
</div>
|
||||
|
||||
<Switch
|
||||
checked={formType === 'report'}
|
||||
onCheckedChange={handleTypeToggle}
|
||||
className="data-[state=checked]:bg-orange-600"
|
||||
/>
|
||||
|
||||
<div className={`flex items-center space-x-2 ${formType === 'report' ? 'text-orange-600 font-medium' : 'text-muted-foreground'}`}>
|
||||
<AlertTriangle className="w-4 h-4" />
|
||||
<span>Report</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Name and Email */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="name">Name <span className="text-red-600">*</span></Label>
|
||||
<Input
|
||||
id="name"
|
||||
type="text"
|
||||
placeholder="Your full name"
|
||||
value={formData.name}
|
||||
onChange={(e) => handleInputChange('name', e.target.value)}
|
||||
className={errors.name ? 'border-red-500' : ''}
|
||||
/>
|
||||
{errors.name && (
|
||||
<p className="text-sm text-red-600">{errors.name}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email <span className="text-red-600">*</span></Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="your.email@example.com"
|
||||
value={formData.email}
|
||||
onChange={(e) => handleInputChange('email', e.target.value)}
|
||||
className={errors.email ? 'border-red-500' : ''}
|
||||
/>
|
||||
{errors.email && (
|
||||
<p className="text-sm text-red-600">{errors.email}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="title">
|
||||
{formType === 'suggestion' ? 'Suggestion Title' : 'Report Title'} <span className="text-red-600">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="title"
|
||||
type="text"
|
||||
placeholder={formType === 'suggestion'
|
||||
? 'Brief title for your suggestion'
|
||||
: 'Brief title for your report'
|
||||
}
|
||||
value={formData.title}
|
||||
onChange={(e) => handleInputChange('title', e.target.value)}
|
||||
className={errors.title ? 'border-red-500' : ''}
|
||||
/>
|
||||
{errors.title && (
|
||||
<p className="text-sm text-red-600">{errors.title}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Category and Urgency */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="category">Category</Label>
|
||||
<Select
|
||||
value={formData.category}
|
||||
onValueChange={(value) => handleInputChange('category', value)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{categoryOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Urgency (only for reports) */}
|
||||
{formType === 'report' && (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="urgency">Urgency Level</Label>
|
||||
<Select
|
||||
value={formData.urgency}
|
||||
onValueChange={(value) => handleInputChange('urgency', value)}
|
||||
>
|
||||
<SelectTrigger className="border-orange-200 focus:border-orange-400">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{urgencyOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Details */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="details">Details <span className="text-red-600">*</span></Label>
|
||||
<Textarea
|
||||
id="details"
|
||||
placeholder={formType === 'suggestion'
|
||||
? 'Please describe your suggestion in detail. What would you like to see improved or added?'
|
||||
: 'Please describe the issue in detail. What happened, and what were you trying to do?'
|
||||
}
|
||||
rows={6}
|
||||
value={formData.details}
|
||||
onChange={(e) => handleInputChange('details', e.target.value)}
|
||||
className={errors.details ? 'border-red-500' : ''}
|
||||
/>
|
||||
{errors.details && (
|
||||
<p className="text-sm text-red-600">{errors.details}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Submit Button */}
|
||||
<Button
|
||||
type="submit"
|
||||
className={`w-full md:w-auto ${formType === 'report' ? 'bg-orange-600 hover:bg-orange-700' : ''}`}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin mr-2" />
|
||||
Submitting...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Send className="w-4 h-4 mr-2" />
|
||||
Submit {formType === 'suggestion' ? 'Suggestion' : 'Report'}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
12
app/feedback/page.tsx
Normal file
12
app/feedback/page.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Metadata } from 'next'
|
||||
import { generateMetadata } from '@/lib/seo'
|
||||
import { FeedbackPageClient } from './feedback-client'
|
||||
|
||||
export const metadata: Metadata = generateMetadata({
|
||||
title: 'Suggestion or Report - SiliconPin',
|
||||
description: 'We value your feedback! Use this form to submit suggestions or report any issues you\'ve encountered.',
|
||||
})
|
||||
|
||||
export default function FeedbackPage() {
|
||||
return <FeedbackPageClient />
|
||||
}
|
||||
163
app/forgot-password/page.tsx
Normal file
163
app/forgot-password/page.tsx
Normal file
@@ -0,0 +1,163 @@
|
||||
'use client'
|
||||
import { useState } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { z } from 'zod'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card'
|
||||
import { Header } from '@/components/header'
|
||||
import { Footer } from '@/components/footer'
|
||||
import { Mail, ArrowLeft } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
|
||||
const ForgotPasswordSchema = z.object({
|
||||
email: z.string().email('Please enter a valid email address'),
|
||||
})
|
||||
|
||||
type ForgotPasswordFormData = z.infer<typeof ForgotPasswordSchema>
|
||||
|
||||
export default function ForgotPasswordPage() {
|
||||
const [isSubmitted, setIsSubmitted] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
getValues,
|
||||
} = useForm<ForgotPasswordFormData>({
|
||||
resolver: zodResolver(ForgotPasswordSchema),
|
||||
})
|
||||
|
||||
const onSubmit = async (data: ForgotPasswordFormData) => {
|
||||
try {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
const response = await fetch('/api/auth/forgot-password', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
if (result.success) {
|
||||
setIsSubmitted(true)
|
||||
} else {
|
||||
setError(result.error?.message || 'Failed to send reset email')
|
||||
}
|
||||
} catch (err) {
|
||||
setError('An error occurred. Please try again.')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<Header />
|
||||
<div className="container mx-auto px-4 pt-24 pb-8">
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-3xl font-bold mb-4">Reset Password</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Enter your email address and we'll send you a link to reset your password
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Card className="w-full max-w-md mx-auto">
|
||||
{isSubmitted ? (
|
||||
<>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-green-600">Check Your Email</CardTitle>
|
||||
<CardDescription>
|
||||
We've sent a password reset link to {getValues('email')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<Mail className="w-8 h-8 text-green-600" />
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
Click the link in the email to reset your password. If you don't see the email,
|
||||
check your spam folder.
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Link href="/auth" className="w-full">
|
||||
<Button variant="outline" className="w-full">
|
||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||
Back to Sign In
|
||||
</Button>
|
||||
</Link>
|
||||
</CardFooter>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CardHeader>
|
||||
<CardTitle>Forgot Password</CardTitle>
|
||||
<CardDescription>
|
||||
Enter your email address to receive a password reset link
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<CardContent className="space-y-4">
|
||||
{error && (
|
||||
<div className="p-3 text-sm text-red-600 bg-red-50 border border-red-200 rounded-md">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email Address</Label>
|
||||
<div className="relative">
|
||||
<Mail className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="your@email.com"
|
||||
className="pl-10"
|
||||
{...register('email')}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
{errors.email && (
|
||||
<p className="text-sm text-red-600">{errors.email.message}</p>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="flex flex-col space-y-4 pt-6">
|
||||
<Button type="submit" className="w-full" disabled={loading}>
|
||||
{loading ? 'Sending...' : 'Send Reset Link'}
|
||||
</Button>
|
||||
|
||||
<Link href="/auth" className="w-full">
|
||||
<Button variant="outline" className="w-full">
|
||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||
Back to Sign In
|
||||
</Button>
|
||||
</Link>
|
||||
</CardFooter>
|
||||
</form>
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
231
app/globals.css
Normal file
231
app/globals.css
Normal file
@@ -0,0 +1,231 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* SiliconPin Design System - Professional hosting platform
|
||||
Modern, trust-building color palette with blue/purple tech theme
|
||||
All colors MUST be HSL.
|
||||
*/
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
/* Main brand colors - professional blue/purple tech palette */
|
||||
--background: 240 10% 98%;
|
||||
--foreground: 234 16% 15%;
|
||||
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 234 16% 15%;
|
||||
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 234 16% 15%;
|
||||
|
||||
/* Primary - Tech blue for CTAs and brand elements */
|
||||
--primary: 214 84% 56%;
|
||||
--primary-foreground: 0 0% 100%;
|
||||
--primary-hover: 214 84% 50%;
|
||||
--primary-glow: 214 100% 70%;
|
||||
|
||||
/* Secondary - Professional purple accent */
|
||||
--secondary: 264 83% 57%;
|
||||
--secondary-foreground: 0 0% 100%;
|
||||
--secondary-hover: 264 83% 50%;
|
||||
|
||||
/* Success - Green for status indicators */
|
||||
--success: 142 76% 36%;
|
||||
--success-foreground: 0 0% 100%;
|
||||
|
||||
/* Warning - Orange for alerts */
|
||||
--warning: 38 92% 50%;
|
||||
--warning-foreground: 0 0% 100%;
|
||||
|
||||
/* Muted colors for subtle elements */
|
||||
--muted: 220 14% 96%;
|
||||
--muted-foreground: 220 8.9% 46.1%;
|
||||
|
||||
/* Accent colors for highlights */
|
||||
--accent: 220 14% 96%;
|
||||
--accent-foreground: 220 8.9% 46.1%;
|
||||
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 100%;
|
||||
|
||||
/* Borders and inputs */
|
||||
--border: 220 13% 91%;
|
||||
--input: 220 13% 91%;
|
||||
--ring: 214 84% 56%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
|
||||
--sidebar-background: 0 0% 98%;
|
||||
--sidebar-foreground: 240 5.3% 26.1%;
|
||||
--sidebar-primary: 240 5.9% 10%;
|
||||
--sidebar-primary-foreground: 0 0% 98%;
|
||||
--sidebar-accent: 240 4.8% 95.9%;
|
||||
--sidebar-accent-foreground: 240 5.9% 10%;
|
||||
--sidebar-border: 220 13% 91%;
|
||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||
}
|
||||
|
||||
.dark {
|
||||
/* Dark theme - professional dark blue with brand consistency */
|
||||
--background: 234 16% 6%;
|
||||
--foreground: 240 10% 95%;
|
||||
|
||||
--card: 234 16% 8%;
|
||||
--card-foreground: 240 10% 95%;
|
||||
|
||||
--popover: 234 16% 8%;
|
||||
--popover-foreground: 240 10% 95%;
|
||||
|
||||
/* Primary stays consistent in dark mode */
|
||||
--primary: 214 84% 56%;
|
||||
--primary-foreground: 0 0% 100%;
|
||||
--primary-hover: 214 84% 60%;
|
||||
--primary-glow: 214 100% 70%;
|
||||
|
||||
/* Secondary adjusted for dark mode */
|
||||
--secondary: 264 83% 62%;
|
||||
--secondary-foreground: 0 0% 100%;
|
||||
--secondary-hover: 264 83% 65%;
|
||||
|
||||
/* Success/Warning adjusted for dark mode */
|
||||
--success: 142 76% 40%;
|
||||
--success-foreground: 0 0% 100%;
|
||||
|
||||
--warning: 38 92% 55%;
|
||||
--warning-foreground: 0 0% 100%;
|
||||
|
||||
--muted: 234 16% 12%;
|
||||
--muted-foreground: 240 5% 65%;
|
||||
|
||||
--accent: 234 16% 12%;
|
||||
--accent-foreground: 240 5% 65%;
|
||||
|
||||
--destructive: 0 62.8% 50%;
|
||||
--destructive-foreground: 0 0% 100%;
|
||||
|
||||
--border: 234 16% 18%;
|
||||
--input: 234 16% 18%;
|
||||
--ring: 214 84% 56%;
|
||||
--sidebar-background: 240 5.9% 10%;
|
||||
--sidebar-foreground: 240 4.8% 95.9%;
|
||||
--sidebar-primary: 224.3 76.3% 48%;
|
||||
--sidebar-primary-foreground: 0 0% 100%;
|
||||
--sidebar-accent: 240 3.7% 15.9%;
|
||||
--sidebar-accent-foreground: 240 4.8% 95.9%;
|
||||
--sidebar-border: 240 3.7% 15.9%;
|
||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Custom gradients and animations for SiliconPin */
|
||||
:root {
|
||||
/* Beautiful gradients using brand colors */
|
||||
--gradient-primary: linear-gradient(135deg, hsl(var(--primary)), hsl(var(--primary-glow)));
|
||||
--gradient-secondary: linear-gradient(135deg, hsl(var(--secondary)), hsl(var(--secondary-hover)));
|
||||
--gradient-hero: linear-gradient(135deg, hsl(var(--primary)), hsl(var(--secondary)));
|
||||
--gradient-glass: linear-gradient(135deg, hsl(var(--background) / 0.8), hsl(var(--muted) / 0.9));
|
||||
|
||||
/* Elegant shadows using brand colors */
|
||||
--shadow-primary: 0 10px 30px -10px hsl(var(--primary) / 0.3);
|
||||
--shadow-secondary: 0 10px 30px -10px hsl(var(--secondary) / 0.3);
|
||||
--shadow-card: 0 4px 12px hsl(var(--foreground) / 0.1);
|
||||
--shadow-glow: 0 0 40px hsl(var(--primary-glow) / 0.4);
|
||||
|
||||
/* Smooth transitions */
|
||||
--transition-smooth: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--transition-bounce: all 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground font-sans;
|
||||
font-feature-settings:
|
||||
'rlig' 1,
|
||||
'calt' 1;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
@apply font-heading font-semibold tracking-tight;
|
||||
font-feature-settings:
|
||||
'rlig' 1,
|
||||
'calt' 1;
|
||||
}
|
||||
|
||||
h1 {
|
||||
@apply text-4xl md:text-5xl lg:text-6xl;
|
||||
}
|
||||
|
||||
h2 {
|
||||
@apply text-3xl md:text-4xl lg:text-5xl;
|
||||
}
|
||||
|
||||
h3 {
|
||||
@apply text-2xl md:text-3xl;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
/* Gradient text utility */
|
||||
.bg-gradient-text {
|
||||
@apply bg-gradient-hero bg-clip-text text-transparent;
|
||||
}
|
||||
|
||||
/* Font optimization utilities */
|
||||
.text-balance {
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
.font-feature-default {
|
||||
font-feature-settings:
|
||||
'kern' 1,
|
||||
'liga' 1,
|
||||
'calt' 1,
|
||||
'pnum' 1,
|
||||
'tnum' 0,
|
||||
'onum' 1,
|
||||
'lnum' 0,
|
||||
'dlig' 0;
|
||||
}
|
||||
|
||||
.font-feature-numeric {
|
||||
font-feature-settings:
|
||||
'kern' 1,
|
||||
'liga' 1,
|
||||
'calt' 1,
|
||||
'pnum' 0,
|
||||
'tnum' 1,
|
||||
'onum' 0,
|
||||
'lnum' 1,
|
||||
'dlig' 0;
|
||||
}
|
||||
|
||||
/* Dev-Portfolio Topic Design System */
|
||||
.container-custom {
|
||||
@apply container px-4 md:px-6 mx-auto max-w-7xl;
|
||||
}
|
||||
|
||||
.section-heading {
|
||||
@apply text-3xl md:text-4xl font-bold mb-8 relative;
|
||||
}
|
||||
|
||||
.section-heading::after {
|
||||
content: "";
|
||||
@apply block w-16 h-1 bg-primary mt-2 mx-auto;
|
||||
}
|
||||
|
||||
.topic-card {
|
||||
@apply bg-card rounded-lg overflow-hidden border border-border hover:shadow-lg transition-all duration-300;
|
||||
}
|
||||
}
|
||||
36
app/icon.tsx
Normal file
36
app/icon.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { ImageResponse } from 'next/og'
|
||||
|
||||
export const runtime = 'edge'
|
||||
|
||||
export const size = {
|
||||
width: 32,
|
||||
height: 32,
|
||||
}
|
||||
|
||||
export const contentType = 'image/png'
|
||||
|
||||
export default function Icon() {
|
||||
return new ImageResponse(
|
||||
(
|
||||
<div
|
||||
style={{
|
||||
fontSize: 18,
|
||||
background: 'linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%)',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: 'white',
|
||||
borderRadius: 6,
|
||||
fontWeight: 'bold',
|
||||
}}
|
||||
>
|
||||
SP
|
||||
</div>
|
||||
),
|
||||
{
|
||||
...size,
|
||||
}
|
||||
)
|
||||
}
|
||||
90
app/layout.tsx
Normal file
90
app/layout.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import type { Metadata } from 'next'
|
||||
import { Inter, Poppins } from 'next/font/google'
|
||||
import { ThemeProvider } from '@/components/theme-provider'
|
||||
import { QueryProvider } from '@/contexts/QueryProvider'
|
||||
import { AuthProvider } from '@/contexts/AuthContext'
|
||||
import { Toaster } from '@/components/ui/toaster'
|
||||
import PWAInstallPrompt from '@/components/PWAInstallPrompt'
|
||||
// Removed StartupTest import - we'll do server-side checks only
|
||||
import './globals.css'
|
||||
|
||||
const inter = Inter({
|
||||
subsets: ['latin'],
|
||||
variable: '--font-inter',
|
||||
display: 'swap',
|
||||
preload: true,
|
||||
weight: ['300', '400', '500', '600', '700', '800'],
|
||||
})
|
||||
|
||||
const poppins = Poppins({
|
||||
subsets: ['latin'],
|
||||
weight: ['300', '400', '500', '600', '700', '800', '900'],
|
||||
variable: '--font-poppins',
|
||||
display: 'swap',
|
||||
preload: true,
|
||||
})
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'SiliconPin - Professional Hosting & Developer Services',
|
||||
description:
|
||||
'Professional web hosting, Kubernetes, VPS, developer services, and web tools. Built by developers, for developers.',
|
||||
manifest: '/manifest.json',
|
||||
themeColor: '#000000',
|
||||
viewport: 'width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no',
|
||||
icons: {
|
||||
icon: [
|
||||
{ url: '/icons/icon-192x192.png', sizes: '192x192', type: 'image/png' },
|
||||
{ url: '/icons/icon-512x512.png', sizes: '512x512', type: 'image/png' },
|
||||
],
|
||||
apple: [{ url: '/icons/icon-192x192.png' }],
|
||||
},
|
||||
appleWebApp: {
|
||||
capable: true,
|
||||
statusBarStyle: 'default',
|
||||
title: 'SiliconPin',
|
||||
},
|
||||
formatDetection: {
|
||||
telephone: false,
|
||||
},
|
||||
openGraph: {
|
||||
type: 'website',
|
||||
siteName: 'SiliconPin',
|
||||
title: 'SiliconPin - Professional Hosting & Developer Services',
|
||||
description:
|
||||
'Professional web hosting, Kubernetes, VPS, developer services, and web tools. Built by developers, for developers.',
|
||||
},
|
||||
twitter: {
|
||||
card: 'summary_large_image',
|
||||
title: 'SiliconPin - Professional Hosting & Developer Services',
|
||||
description:
|
||||
'Professional web hosting, Kubernetes, VPS, developer services, and web tools. Built by developers, for developers.',
|
||||
},
|
||||
}
|
||||
|
||||
export default async function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
// Run startup checks only during runtime (not build time)
|
||||
if (typeof window === 'undefined' && process.env.NEXT_PHASE !== 'phase-production-build') {
|
||||
try {
|
||||
const { runStartupChecks } = await import('@/lib/startup')
|
||||
await runStartupChecks()
|
||||
} catch (error) {
|
||||
console.log('Startup checks failed, continuing anyway...')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body className={`${inter.variable} ${poppins.variable} antialiased`}>
|
||||
<ThemeProvider defaultTheme="system">
|
||||
<QueryProvider>
|
||||
<AuthProvider>
|
||||
{children}
|
||||
<Toaster />
|
||||
<PWAInstallPrompt />
|
||||
</AuthProvider>
|
||||
</QueryProvider>
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
371
app/legal-agreement/page.tsx
Normal file
371
app/legal-agreement/page.tsx
Normal file
@@ -0,0 +1,371 @@
|
||||
import { Metadata } from 'next'
|
||||
import { generateMetadata } from '@/lib/seo'
|
||||
import { Header } from '@/components/header'
|
||||
import { Footer } from '@/components/footer'
|
||||
import { CustomSolutionCTA } from '@/components/ui/custom-solution-cta'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { ScrollText, Users, FileX, Copyright, Shield, Gavel, FileText, MessageCircle } from 'lucide-react'
|
||||
|
||||
export const metadata: Metadata = generateMetadata({
|
||||
title: 'Legal Agreement - SiliconPin',
|
||||
description:
|
||||
'Review SiliconPin\'s legal agreement for using our services. Find information about your legal rights and obligations.',
|
||||
})
|
||||
|
||||
export default function LegalAgreementPage() {
|
||||
const lastUpdated = "March 18, 2025"
|
||||
|
||||
const terminationReasons = [
|
||||
'You breach any provision of this Agreement',
|
||||
'You fail to pay any amounts due to SiliconPin',
|
||||
'Your use of the Services poses a security risk',
|
||||
'Providing the Services to you becomes unlawful',
|
||||
'Your use of the Services adversely impacts SiliconPin\'s systems or other customers'
|
||||
]
|
||||
|
||||
const customerWarranties = [
|
||||
'You have the legal capacity to enter into this Agreement',
|
||||
'Your use of the Services will comply with all applicable laws and regulations',
|
||||
'Your Content does not infringe or misappropriate any third party\'s intellectual property rights',
|
||||
'Your Content does not contain any material that is defamatory, obscene, or otherwise unlawful'
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<Header />
|
||||
<main className="container max-w-4xl pt-24 pb-8">
|
||||
{/* Page Header */}
|
||||
<div className="text-center mb-12">
|
||||
<h1 className="text-4xl font-bold tracking-tight mb-4">Legal Agreement</h1>
|
||||
<p className="text-xl text-muted-foreground max-w-3xl mx-auto">
|
||||
Terms of our legal relationship
|
||||
</p>
|
||||
<div className="mt-4">
|
||||
<Badge variant="outline" className="text-sm">
|
||||
<ScrollText className="w-4 h-4 mr-2" />
|
||||
Last Updated: {lastUpdated}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-8">
|
||||
{/* TLDR Section */}
|
||||
<Card className="border-green-200 dark:border-green-800">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl flex items-center text-green-600">
|
||||
<MessageCircle className="w-6 h-6 mr-2" />
|
||||
TLDR;
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="bg-green-50 dark:bg-green-950/10 p-4 rounded-lg border border-green-200 dark:border-green-800">
|
||||
<p className="text-green-800 dark:text-green-200 leading-relaxed">
|
||||
We are trying to create some digital freedom together. We avoid any vendor locking - for you and us.
|
||||
Some of us from DWD Consultancy Services, having quite experience manipulating data and data resilience.
|
||||
Still we advise to keep a backup of your data periodically and use our automated backup and snapshot services
|
||||
(even AWS/GCP don't take data responsibility). You are responsible for your content, and we are responsible for our services.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p className="text-muted-foreground leading-relaxed">
|
||||
If something goes wrong, we will try to fix it. For SLA we will refund or we will part ways.
|
||||
We are based in India, and Indian law applies to this agreement. We find some law funny and useless like the cookie acknowledgement.
|
||||
</p>
|
||||
|
||||
<p className="text-muted-foreground leading-relaxed font-medium">
|
||||
We strongly feel just to be reasonable and fair to each other.
|
||||
</p>
|
||||
|
||||
<hr className="my-4" />
|
||||
|
||||
<p className="text-muted-foreground leading-relaxed text-sm">
|
||||
This Legal Agreement ("Agreement") is a binding contract between you ("Customer" or "you") and SiliconPin
|
||||
("Company", "we", or "us") governing your use of our hosting services and related products (collectively, the "Services").
|
||||
By using our Services, you acknowledge that you have read, understood, and agree to be bound by this Agreement.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Relationship of Parties */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl flex items-center">
|
||||
<Users className="w-6 h-6 mr-2 text-blue-600" />
|
||||
Relationship of Parties
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<p className="text-muted-foreground leading-relaxed">
|
||||
The relationship between you and SiliconPin is that of an independent contractor. Nothing in this Agreement
|
||||
shall be construed as creating a partnership, joint venture, agency, employment, or fiduciary relationship
|
||||
between the parties.
|
||||
</p>
|
||||
<p className="text-muted-foreground leading-relaxed">
|
||||
You acknowledge that SiliconPin has no control over the content of information transmitted by you through our
|
||||
Services and that SiliconPin does not examine the use to which you put the Services or the nature of the content
|
||||
or information you are transmitting.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Term and Termination */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl flex items-center">
|
||||
<FileX className="w-6 h-6 mr-2 text-red-600" />
|
||||
Term and Termination
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-green-600 mb-3">2.1 Term</h3>
|
||||
<p className="text-muted-foreground leading-relaxed">
|
||||
This Agreement begins on the date you first use our Services and continues until terminated as described herein.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-green-600 mb-3">2.2 Termination by Customer</h3>
|
||||
<p className="text-muted-foreground leading-relaxed">
|
||||
You may terminate this Agreement at any time by canceling your account through your control panel or by
|
||||
contacting our customer support. Upon termination, you will be responsible for all fees incurred up to the
|
||||
date of termination.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-green-600 mb-3">2.3 Termination by SiliconPin</h3>
|
||||
<p className="text-muted-foreground mb-3">
|
||||
SiliconPin may terminate this Agreement at any time if:
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
{terminationReasons.map((reason, index) => (
|
||||
<div key={index} className="flex items-start space-x-3">
|
||||
<div className="w-2 h-2 bg-red-600 rounded-full mt-2 shrink-0"></div>
|
||||
<span className="text-muted-foreground">{reason}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Intellectual Property */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl flex items-center">
|
||||
<Copyright className="w-6 h-6 mr-2 text-purple-600" />
|
||||
Intellectual Property
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-green-600 mb-3">3.1 Your Content</h3>
|
||||
<p className="text-muted-foreground leading-relaxed">
|
||||
You retain all right, title, and interest in and to any content that you create, upload, post, transmit,
|
||||
or otherwise make available through the Services ("Your Content"). By using our Services, you grant SiliconPin
|
||||
a non-exclusive, worldwide, royalty-free license to use, store, and copy Your Content solely for the purpose
|
||||
of providing the Services to you.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-green-600 mb-3">3.2 SiliconPin's Intellectual Property</h3>
|
||||
<p className="text-muted-foreground leading-relaxed">
|
||||
All intellectual property rights in the Services, including but not limited to software, logos, trademarks,
|
||||
and documentation, are owned by SiliconPin or its licensors. Nothing in this Agreement transfers any of
|
||||
SiliconPin's intellectual property rights to you.
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Warranties and Disclaimers */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl flex items-center">
|
||||
<Shield className="w-6 h-6 mr-2 text-orange-600" />
|
||||
Warranties and Disclaimers
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-green-600 mb-3">4.1 Customer Warranties</h3>
|
||||
<p className="text-muted-foreground mb-3">
|
||||
You represent and warrant that:
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
{customerWarranties.map((warranty, index) => (
|
||||
<div key={index} className="flex items-start space-x-3">
|
||||
<div className="w-2 h-2 bg-green-600 rounded-full mt-2 shrink-0"></div>
|
||||
<span className="text-muted-foreground">{warranty}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-green-600 mb-3">4.2 Disclaimer of Warranties</h3>
|
||||
<div className="p-4 bg-orange-50 dark:bg-orange-950/10 rounded-lg border border-orange-200 dark:border-orange-800">
|
||||
<p className="text-orange-800 dark:text-orange-200 leading-relaxed text-sm font-medium">
|
||||
THE SERVICES ARE PROVIDED "AS IS" AND "AS AVAILABLE," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE,
|
||||
NON-INFRINGEMENT, AND ANY WARRANTY ARISING FROM COURSE OF DEALING OR USAGE OF TRADE. SILICONPIN DOES NOT
|
||||
WARRANT THAT THE SERVICES WILL BE UNINTERRUPTED, ERROR-FREE, OR COMPLETELY SECURE.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Limitation of Liability */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl">Limitation of Liability</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="p-4 bg-red-50 dark:bg-red-950/10 rounded-lg border border-red-200 dark:border-red-800">
|
||||
<p className="text-red-800 dark:text-red-200 leading-relaxed text-sm font-medium">
|
||||
IN NO EVENT WILL SILICONPIN BE LIABLE TO YOU OR ANY THIRD PARTY FOR ANY INDIRECT, CONSEQUENTIAL, EXEMPLARY,
|
||||
INCIDENTAL, SPECIAL, OR PUNITIVE DAMAGES, INCLUDING LOST PROFIT, LOST REVENUE, LOSS OF DATA, OR OTHER DAMAGES
|
||||
ARISING FROM YOUR USE OF THE SERVICES, EVEN IF SILICONPIN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p className="text-muted-foreground leading-relaxed">
|
||||
NOTWITHSTANDING ANYTHING TO THE CONTRARY, SILICONPIN'S LIABILITY TO YOU FOR ANY CAUSE WHATSOEVER AND REGARDLESS
|
||||
OF THE FORM OF THE ACTION, WILL AT ALL TIMES BE LIMITED TO THE AMOUNT PAID, IF ANY, BY YOU TO SILICONPIN FOR THE
|
||||
SERVICES DURING THE PERIOD OF THREE (3) MONTHS PRIOR TO ANY CAUSE OF ACTION ARISING.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Indemnification */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl">Indemnification</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-muted-foreground leading-relaxed">
|
||||
You agree to defend, indemnify, and hold harmless SiliconPin and its officers, directors, employees, and agents
|
||||
from and against any and all claims, damages, obligations, losses, liabilities, costs, or debt, and expenses
|
||||
(including but not limited to attorney's fees) arising from: (i) your use of and access to the Services;
|
||||
(ii) your violation of any term of this Agreement; (iii) your violation of any third-party right, including
|
||||
without limitation any copyright, property, or privacy right; or (iv) any claim that Your Content caused damage to a third party.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Dispute Resolution */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl flex items-center">
|
||||
<Gavel className="w-6 h-6 mr-2 text-blue-600" />
|
||||
Dispute Resolution
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-green-600 mb-3">7.1 Governing Law</h3>
|
||||
<p className="text-muted-foreground leading-relaxed">
|
||||
This Agreement shall be governed by and construed in accordance with the laws of India, without regard to
|
||||
its conflict of law provisions.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-green-600 mb-3">7.2 Arbitration</h3>
|
||||
<p className="text-muted-foreground leading-relaxed">
|
||||
Any dispute arising out of or in connection with this Agreement, including any question regarding its existence,
|
||||
validity, or termination, shall be referred to and finally resolved by arbitration under the rules of the
|
||||
Indian Arbitration and Conciliation Act, 1996, which rules are deemed to be incorporated by reference into this clause.
|
||||
The number of arbitrators shall be one. The seat, or legal place, of arbitration shall be Kolkata, India.
|
||||
The language to be used in the arbitral proceedings shall be English.
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* General Provisions */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl flex items-center">
|
||||
<FileText className="w-6 h-6 mr-2 text-gray-600" />
|
||||
General Provisions
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-green-600 mb-3">8.1 Entire Agreement</h3>
|
||||
<p className="text-muted-foreground leading-relaxed">
|
||||
This Agreement constitutes the entire agreement between you and SiliconPin regarding the Services and
|
||||
supersedes all prior agreements and understandings, whether written or oral.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-green-600 mb-3">8.2 Severability</h3>
|
||||
<p className="text-muted-foreground leading-relaxed">
|
||||
If any provision of this Agreement is found to be unenforceable or invalid, that provision will be limited
|
||||
or eliminated to the minimum extent necessary so that this Agreement will otherwise remain in full force and effect.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-green-600 mb-3">8.3 Assignment</h3>
|
||||
<p className="text-muted-foreground leading-relaxed">
|
||||
You may not assign or transfer this Agreement without SiliconPin's prior written consent. SiliconPin may
|
||||
assign or transfer this Agreement without your consent.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-green-600 mb-3">8.4 No Waiver</h3>
|
||||
<p className="text-muted-foreground leading-relaxed">
|
||||
The failure of SiliconPin to enforce any right or provision of this Agreement will not be deemed a waiver
|
||||
of such right or provision.
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Contact Information */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl">Contact Information</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-muted-foreground leading-relaxed mb-4">
|
||||
If you have any questions about this Legal Agreement, please contact us:
|
||||
</p>
|
||||
<div className="space-y-2 text-muted-foreground">
|
||||
<p>
|
||||
Email:{' '}
|
||||
<a href="mailto:contact@siliconpin.com" className="text-green-600 hover:underline font-medium">
|
||||
contact@siliconpin.com
|
||||
</a>
|
||||
</p>
|
||||
<p>Phone: +91-700-160-1485</p>
|
||||
<p>
|
||||
Address: 121 Lalbari, GourBongo Road<br />
|
||||
Habra, West Bengal 743271<br />
|
||||
India
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* CTA Section */}
|
||||
<CustomSolutionCTA
|
||||
title="Need Legal Assistance?"
|
||||
description="Have questions about this agreement or need clarification on any legal terms? Our team is here to help"
|
||||
buttonText="Contact Legal Team"
|
||||
buttonHref="mailto:contact@siliconpin.com"
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
71
app/not-found.tsx
Normal file
71
app/not-found.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import Link from 'next/link'
|
||||
import { Header } from '@/components/header'
|
||||
import { Footer } from '@/components/footer'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
import { Home, Search, ArrowLeft, Server } from 'lucide-react'
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<div className="min-h-screen bg-background flex flex-col">
|
||||
<Header />
|
||||
<main className="flex-1 flex items-center justify-center py-12">
|
||||
<div className="container max-w-2xl text-center">
|
||||
{/* 404 Visual */}
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center justify-center space-x-4 mb-6">
|
||||
<div className="w-16 h-16 bg-gradient-hero rounded-lg flex items-center justify-center">
|
||||
<Server className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
<div className="text-6xl font-bold text-primary">404</div>
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold tracking-tight mb-4">Page Not Found</h1>
|
||||
<p className="text-xl text-muted-foreground mb-8">
|
||||
Oops! The page you're looking for seems to have wandered off into the digital void.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Helpful Actions */}
|
||||
<Card>
|
||||
<CardContent className="pt-6 space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<Button asChild className="h-12">
|
||||
<Link href="/">
|
||||
<Home className="w-4 h-4 mr-2" />
|
||||
Go Home
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
<Button asChild variant="outline" className="h-12">
|
||||
<Link href="/services">
|
||||
<Search className="w-4 h-4 mr-2" />
|
||||
Browse Services
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-muted-foreground">
|
||||
<p className="mb-2">Common pages you might be looking for:</p>
|
||||
<div className="flex flex-wrap justify-center gap-4">
|
||||
<Link href="/about" className="text-primary hover:underline">About Us</Link>
|
||||
<Link href="/contact" className="text-primary hover:underline">Contact</Link>
|
||||
<Link href="/tools" className="text-primary hover:underline">Web Tools</Link>
|
||||
<Link href="/auth" className="text-primary hover:underline">Sign In</Link>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Fun Error Message */}
|
||||
<div className="mt-8 p-4 bg-muted rounded-lg">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<strong>Error Code:</strong> 404 - Page Not Found<br />
|
||||
<strong>Suggestion:</strong> Check the URL for typos or use the navigation menu above
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
1008
app/page.tsx
Normal file
1008
app/page.tsx
Normal file
File diff suppressed because it is too large
Load Diff
303
app/payment/failed/page.tsx
Normal file
303
app/payment/failed/page.tsx
Normal file
@@ -0,0 +1,303 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState, Suspense } from 'react'
|
||||
import { useSearchParams, useRouter } from 'next/navigation'
|
||||
import { XCircle, RotateCcw, Home, AlertTriangle, HelpCircle, CreditCard } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
|
||||
interface FailureDetails {
|
||||
txn?: string
|
||||
amount?: string
|
||||
type?: string
|
||||
reason?: string
|
||||
error?: string
|
||||
message?: string
|
||||
}
|
||||
|
||||
interface FailureInfo {
|
||||
title: string
|
||||
description: string
|
||||
suggestions: string[]
|
||||
icon: typeof XCircle
|
||||
color: string
|
||||
}
|
||||
|
||||
const FAILURE_REASONS: Record<string, FailureInfo> = {
|
||||
'insufficient-funds': {
|
||||
title: 'Insufficient Funds',
|
||||
description: 'Your payment method does not have enough balance to complete this transaction.',
|
||||
suggestions: [
|
||||
'Add money to your payment account',
|
||||
'Try a different payment method',
|
||||
'Contact your bank if you believe this is an error'
|
||||
],
|
||||
icon: CreditCard,
|
||||
color: 'text-red-500'
|
||||
},
|
||||
'card-declined': {
|
||||
title: 'Card Declined',
|
||||
description: 'Your card was declined by the bank or payment processor.',
|
||||
suggestions: [
|
||||
'Check your card details are correct',
|
||||
'Ensure your card is not expired',
|
||||
'Try a different card',
|
||||
'Contact your bank for more information'
|
||||
],
|
||||
icon: XCircle,
|
||||
color: 'text-red-500'
|
||||
},
|
||||
'timeout': {
|
||||
title: 'Payment Timeout',
|
||||
description: 'The payment took too long to process and timed out.',
|
||||
suggestions: [
|
||||
'Check your internet connection',
|
||||
'Try again with a stable connection',
|
||||
'Clear your browser cache and cookies'
|
||||
],
|
||||
icon: AlertTriangle,
|
||||
color: 'text-orange-500'
|
||||
},
|
||||
'user-cancelled': {
|
||||
title: 'Payment Cancelled',
|
||||
description: 'The payment was cancelled by you during the process.',
|
||||
suggestions: [
|
||||
'You can try the payment again',
|
||||
'Make sure to complete the payment process',
|
||||
'Contact support if you need assistance'
|
||||
],
|
||||
icon: XCircle,
|
||||
color: 'text-gray-500'
|
||||
},
|
||||
'invalid-details': {
|
||||
title: 'Invalid Payment Details',
|
||||
description: 'The payment details provided were invalid or incorrect.',
|
||||
suggestions: [
|
||||
'Double-check your card number and CVV',
|
||||
'Verify the expiry date is correct',
|
||||
'Ensure billing address matches your card'
|
||||
],
|
||||
icon: AlertTriangle,
|
||||
color: 'text-yellow-500'
|
||||
},
|
||||
'unknown': {
|
||||
title: 'Payment Failed',
|
||||
description: 'An unexpected error occurred while processing your payment.',
|
||||
suggestions: [
|
||||
'Try again in a few minutes',
|
||||
'Use a different payment method',
|
||||
'Contact support if the problem persists'
|
||||
],
|
||||
icon: HelpCircle,
|
||||
color: 'text-gray-500'
|
||||
}
|
||||
}
|
||||
|
||||
function PaymentFailedContent() {
|
||||
const searchParams = useSearchParams()
|
||||
const router = useRouter()
|
||||
const [details, setDetails] = useState<FailureDetails>({})
|
||||
const [failureInfo, setFailureInfo] = useState<FailureInfo>(FAILURE_REASONS.unknown)
|
||||
const [countdown, setCountdown] = useState(15)
|
||||
|
||||
useEffect(() => {
|
||||
// Extract failure details from URL parameters
|
||||
const txn = searchParams.get('txn')
|
||||
const amount = searchParams.get('amount')
|
||||
const type = searchParams.get('type') || 'billing'
|
||||
const reason = searchParams.get('reason') || 'unknown'
|
||||
const error = searchParams.get('error')
|
||||
const message = searchParams.get('message')
|
||||
|
||||
const failureDetails: FailureDetails = {
|
||||
txn: txn || undefined,
|
||||
amount: amount || undefined,
|
||||
type,
|
||||
reason,
|
||||
error: error || undefined,
|
||||
message: message ? decodeURIComponent(message) : undefined
|
||||
}
|
||||
|
||||
setDetails(failureDetails)
|
||||
setFailureInfo(FAILURE_REASONS[reason] || FAILURE_REASONS.unknown)
|
||||
|
||||
// Auto-redirect countdown
|
||||
const timer = setInterval(() => {
|
||||
setCountdown((prev) => {
|
||||
if (prev <= 1) {
|
||||
router.push('/profile')
|
||||
return 0
|
||||
}
|
||||
return prev - 1
|
||||
})
|
||||
}, 1000)
|
||||
|
||||
return () => clearInterval(timer)
|
||||
}, [searchParams, router])
|
||||
|
||||
const handleRetryPayment = () => {
|
||||
if (details.type === 'balance') {
|
||||
router.push('/profile?tab=balance')
|
||||
} else {
|
||||
router.push('/services')
|
||||
}
|
||||
}
|
||||
|
||||
const handleDashboardRedirect = () => {
|
||||
router.push('/profile')
|
||||
}
|
||||
|
||||
const handleContactSupport = () => {
|
||||
// TODO: Implement support contact functionality
|
||||
router.push('/contact')
|
||||
}
|
||||
|
||||
const IconComponent = failureInfo.icon
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-red-50 to-orange-50 dark:from-red-900/10 dark:to-orange-900/10 flex items-center justify-center p-4">
|
||||
<Card className="w-full max-w-lg">
|
||||
<CardContent className="p-8 text-center">
|
||||
{/* Failure Icon */}
|
||||
<div className="mb-6">
|
||||
<div className="mx-auto w-16 h-16 bg-red-100 dark:bg-red-900/30 rounded-full flex items-center justify-center">
|
||||
<IconComponent className={`w-10 h-10 ${failureInfo.color}`} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Failure Message */}
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100 mb-2">
|
||||
{failureInfo.title}
|
||||
</h1>
|
||||
|
||||
<p className="text-gray-600 dark:text-gray-400 mb-6">
|
||||
{details.message || failureInfo.description}
|
||||
</p>
|
||||
|
||||
{/* Payment Details (if available) */}
|
||||
{details.txn && (
|
||||
<div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-4 mb-6 space-y-3">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">Transaction ID</span>
|
||||
<code className="text-sm font-mono text-gray-900 dark:text-gray-100">{details.txn}</code>
|
||||
</div>
|
||||
{details.amount && (
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">Amount</span>
|
||||
<span className="text-lg font-bold text-gray-900 dark:text-gray-100">
|
||||
₹{parseFloat(details.amount).toLocaleString('en-IN', { minimumFractionDigits: 2 })}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">Type</span>
|
||||
<span className="text-sm capitalize text-gray-900 dark:text-gray-100">
|
||||
{details.type === 'balance' ? 'Balance Addition' : 'Service Payment'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">Date</span>
|
||||
<span className="text-sm text-gray-900 dark:text-gray-100">
|
||||
{new Date().toLocaleDateString('en-IN', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex flex-col sm:flex-row gap-3 mb-6">
|
||||
<Button
|
||||
onClick={handleRetryPayment}
|
||||
className="flex-1"
|
||||
>
|
||||
<RotateCcw className="w-4 h-4 mr-2" />
|
||||
Try Again
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleDashboardRedirect}
|
||||
variant="outline"
|
||||
className="flex-1"
|
||||
>
|
||||
<Home className="w-4 h-4 mr-2" />
|
||||
Dashboard
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Suggestions */}
|
||||
<div className="mb-6 p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
|
||||
<h3 className="text-sm font-medium text-blue-900 dark:text-blue-100 mb-2">
|
||||
What can you do?
|
||||
</h3>
|
||||
<ul className="text-sm text-blue-700 dark:text-blue-300 space-y-1 text-left">
|
||||
{failureInfo.suggestions.map((suggestion, index) => (
|
||||
<li key={index} className="flex items-start">
|
||||
<span className="mr-2 mt-0.5">•</span>
|
||||
<span>{suggestion}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Support Section */}
|
||||
<div className="mb-6 p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
|
||||
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-2">
|
||||
Need Help?
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
|
||||
If you continue to experience issues, our support team is here to help.
|
||||
</p>
|
||||
<Button
|
||||
onClick={handleContactSupport}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
>
|
||||
Contact Support
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Auto-redirect Notice */}
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Redirecting to dashboard in {countdown} seconds
|
||||
</p>
|
||||
|
||||
{/* Error Details (for debugging, only show in development) */}
|
||||
{details.error && process.env.NODE_ENV === 'development' && (
|
||||
<details className="mt-4 text-left">
|
||||
<summary className="text-sm text-gray-500 cursor-pointer">
|
||||
Debug Information
|
||||
</summary>
|
||||
<pre className="mt-2 p-2 bg-gray-100 dark:bg-gray-800 rounded text-xs overflow-auto">
|
||||
{JSON.stringify({ details, failureInfo }, null, 2)}
|
||||
</pre>
|
||||
</details>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function PaymentFailedPage() {
|
||||
return (
|
||||
<Suspense fallback={
|
||||
<div className="min-h-screen bg-gradient-to-br from-red-50 to-red-100 dark:from-red-950 dark:to-red-900 flex items-center justify-center p-4">
|
||||
<Card className="w-full max-w-md mx-auto">
|
||||
<CardContent className="p-8">
|
||||
<div className="text-center space-y-4">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-red-600 mx-auto"></div>
|
||||
<p className="text-gray-600">Loading...</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
}>
|
||||
<PaymentFailedContent />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
198
app/payment/success/page.tsx
Normal file
198
app/payment/success/page.tsx
Normal file
@@ -0,0 +1,198 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState, Suspense } from 'react'
|
||||
import { useSearchParams, useRouter } from 'next/navigation'
|
||||
import { CheckCircle, Download, ArrowRight, Home, Receipt } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
|
||||
interface PaymentDetails {
|
||||
txn: string
|
||||
amount: string
|
||||
type?: string
|
||||
warning?: string
|
||||
}
|
||||
|
||||
// Main payment success component with search params handling
|
||||
function PaymentSuccessContent() {
|
||||
const searchParams = useSearchParams()
|
||||
const router = useRouter()
|
||||
const [details, setDetails] = useState<PaymentDetails | null>(null)
|
||||
const [countdown, setCountdown] = useState(10)
|
||||
|
||||
useEffect(() => {
|
||||
// Extract payment details from URL parameters
|
||||
const txn = searchParams.get('txn')
|
||||
const amount = searchParams.get('amount')
|
||||
const type = searchParams.get('type') || 'billing'
|
||||
const warning = searchParams.get('warning')
|
||||
|
||||
if (txn && amount) {
|
||||
setDetails({
|
||||
txn,
|
||||
amount,
|
||||
type,
|
||||
warning: warning || undefined,
|
||||
})
|
||||
}
|
||||
|
||||
// Auto-redirect countdown
|
||||
const timer = setInterval(() => {
|
||||
setCountdown((prev) => {
|
||||
if (prev <= 1) {
|
||||
router.push('/profile')
|
||||
return 0
|
||||
}
|
||||
return prev - 1
|
||||
})
|
||||
}, 1000)
|
||||
|
||||
return () => clearInterval(timer)
|
||||
}, [searchParams, router])
|
||||
|
||||
const handleDashboardRedirect = () => {
|
||||
router.push('/profile')
|
||||
}
|
||||
|
||||
const handleDownloadReceipt = () => {
|
||||
// TODO: Implement receipt download
|
||||
// In real implementation, this would generate and download a PDF receipt
|
||||
console.log('Downloading receipt for transaction:', details?.txn)
|
||||
alert('Receipt download feature will be implemented')
|
||||
}
|
||||
|
||||
if (!details) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-green-500 mx-auto mb-4"></div>
|
||||
<p>Loading payment details...</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-green-50 to-blue-50 dark:from-green-900/10 dark:to-blue-900/10 flex items-center justify-center p-4">
|
||||
<Card className="w-full max-w-lg">
|
||||
<CardContent className="p-8 text-center">
|
||||
{/* Success Icon */}
|
||||
<div className="mb-6">
|
||||
<div className="mx-auto w-16 h-16 bg-green-100 dark:bg-green-900/30 rounded-full flex items-center justify-center">
|
||||
<CheckCircle className="w-10 h-10 text-green-600 dark:text-green-400" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Success Message */}
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100 mb-2">
|
||||
Payment Successful!
|
||||
</h1>
|
||||
|
||||
<p className="text-gray-600 dark:text-gray-400 mb-6">
|
||||
{details.type === 'balance'
|
||||
? 'Your account balance has been updated successfully.'
|
||||
: 'Your payment has been processed successfully.'}
|
||||
</p>
|
||||
|
||||
{/* Warning Message (if any) */}
|
||||
{details.warning && (
|
||||
<div className="mb-6 p-4 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-700 rounded-md">
|
||||
<p className="text-sm text-yellow-800 dark:text-yellow-200">
|
||||
⚠️ Payment successful, but there was an issue updating our records. Our team has
|
||||
been notified and will resolve this shortly.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Payment Details */}
|
||||
<div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-4 mb-6 space-y-3">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">Transaction ID</span>
|
||||
<code className="text-sm font-mono text-gray-900 dark:text-gray-100">
|
||||
{details.txn}
|
||||
</code>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">Amount</span>
|
||||
<span className="text-lg font-bold text-green-600 dark:text-green-400">
|
||||
₹{parseFloat(details.amount).toLocaleString('en-IN', { minimumFractionDigits: 2 })}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">Type</span>
|
||||
<span className="text-sm capitalize text-gray-900 dark:text-gray-100">
|
||||
{details.type === 'balance' ? 'Balance Addition' : 'Service Payment'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">Date</span>
|
||||
<span className="text-sm text-gray-900 dark:text-gray-100">
|
||||
{new Date().toLocaleDateString('en-IN', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex flex-col sm:flex-row gap-3 mb-6">
|
||||
<Button onClick={handleDownloadReceipt} variant="outline" className="flex-1">
|
||||
<Receipt className="w-4 h-4 mr-2" />
|
||||
Download Receipt
|
||||
</Button>
|
||||
<Button onClick={handleDashboardRedirect} className="flex-1">
|
||||
<Home className="w-4 h-4 mr-2" />
|
||||
Go to Dashboard
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Auto-redirect Notice */}
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Redirecting to dashboard in {countdown} seconds
|
||||
</p>
|
||||
|
||||
{/* Next Steps */}
|
||||
<div className="mt-6 p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
|
||||
<h3 className="text-sm font-medium text-blue-900 dark:text-blue-100 mb-2">
|
||||
What's Next?
|
||||
</h3>
|
||||
<div className="text-sm text-blue-700 dark:text-blue-300 space-y-1">
|
||||
{details.type === 'balance' ? (
|
||||
<>
|
||||
<p>• Your account balance has been updated</p>
|
||||
<p>• You can now use your balance for services</p>
|
||||
<p>• Check your transaction history in your profile</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p>• Your service will be activated shortly</p>
|
||||
<p>• Check your email for service details</p>
|
||||
<p>• View service status in your dashboard</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Wrapper component with Suspense boundary
|
||||
export default function PaymentSuccessPage() {
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-primary"></div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<PaymentSuccessContent />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
337
app/privacy/page.tsx
Normal file
337
app/privacy/page.tsx
Normal file
@@ -0,0 +1,337 @@
|
||||
import { Metadata } from 'next'
|
||||
import { generateMetadata } from '@/lib/seo'
|
||||
import { Header } from '@/components/header'
|
||||
import { Footer } from '@/components/footer'
|
||||
import { CustomSolutionCTA } from '@/components/ui/custom-solution-cta'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Shield, Database, Eye, Users, FileText, Trash2 } from 'lucide-react'
|
||||
|
||||
export const metadata: Metadata = generateMetadata({
|
||||
title: 'Privacy Policy - SiliconPin',
|
||||
description:
|
||||
'Learn how SiliconPin collects, uses, and protects your personal information. Our privacy policy outlines our data practices and your rights.',
|
||||
})
|
||||
|
||||
export default function PrivacyPage() {
|
||||
const lastUpdated = "March 18, 2025"
|
||||
|
||||
const informationTypes = {
|
||||
personal: [
|
||||
'Name',
|
||||
'Email address',
|
||||
'Phone number',
|
||||
'Billing address',
|
||||
'Payment information',
|
||||
'Company information',
|
||||
'Account credentials'
|
||||
],
|
||||
usage: [
|
||||
'Log files',
|
||||
'IP addresses',
|
||||
'Browser type',
|
||||
'Pages visited',
|
||||
'Time spent on pages',
|
||||
'Referring website addresses',
|
||||
'Resource usage statistics'
|
||||
]
|
||||
}
|
||||
|
||||
const dataUses = [
|
||||
'Providing, maintaining, and improving our services',
|
||||
'Processing and completing transactions',
|
||||
'Sending administrative information, such as service updates and security alerts',
|
||||
'Responding to your comments, questions, and requests',
|
||||
'Sending promotional materials and newsletters (with opt-out options)',
|
||||
'Monitoring and analyzing trends, usage, and activities',
|
||||
'Detecting, preventing, and addressing technical issues',
|
||||
'Complying with legal obligations'
|
||||
]
|
||||
|
||||
const userRights = [
|
||||
'Access',
|
||||
'Correction',
|
||||
'Deletion',
|
||||
'Objection',
|
||||
'Data portability',
|
||||
'Withdraw consent'
|
||||
]
|
||||
|
||||
const sharingScenarios = [
|
||||
{
|
||||
title: 'With Service Providers',
|
||||
description: 'We may share your information with third-party service providers who perform services on our behalf.'
|
||||
},
|
||||
{
|
||||
title: 'For Legal Reasons',
|
||||
description: 'We may disclose your information if required to do so by law.'
|
||||
},
|
||||
{
|
||||
title: 'Business Transfers',
|
||||
description: 'In the event of a merger or acquisition, your information may be transferred.'
|
||||
},
|
||||
{
|
||||
title: 'With Your Consent',
|
||||
description: 'We may share your information with your permission.'
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<Header />
|
||||
<main className="container max-w-4xl pt-24 pb-8">
|
||||
{/* Page Header */}
|
||||
<div className="text-center mb-12">
|
||||
<h1 className="text-4xl font-bold tracking-tight mb-4">Privacy Policy</h1>
|
||||
<p className="text-xl text-muted-foreground max-w-3xl mx-auto">
|
||||
How we collect, use, and protect your information
|
||||
</p>
|
||||
<div className="mt-4">
|
||||
<Badge variant="outline" className="text-sm">
|
||||
<Shield className="w-4 h-4 mr-2" />
|
||||
Last Updated: {lastUpdated}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-8">
|
||||
{/* Introduction */}
|
||||
<Card>
|
||||
<CardContent className="pt-6 space-y-4">
|
||||
<p className="text-muted-foreground leading-relaxed">
|
||||
At SiliconPin, we are committed to protecting your privacy and ensuring the security of your personal information.
|
||||
This Privacy Policy explains how we collect, use, disclose, and safeguard your information when you use our hosting
|
||||
services or visit our website.
|
||||
</p>
|
||||
<p className="text-muted-foreground leading-relaxed">
|
||||
Please read this Privacy Policy carefully. By accessing or using our services, you acknowledge that you have read,
|
||||
understood, and agree to be bound by all the terms of this Privacy Policy.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Information We Collect */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl flex items-center">
|
||||
<Database className="w-6 h-6 mr-2 text-blue-600" />
|
||||
Information We Collect
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-green-600 mb-3">1.1 Personal Information</h3>
|
||||
<p className="text-muted-foreground mb-3">
|
||||
We may collect personal information that you provide directly to us, such as:
|
||||
</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||
{informationTypes.personal.map((item, index) => (
|
||||
<div key={index} className="flex items-center space-x-3">
|
||||
<div className="w-2 h-2 bg-green-600 rounded-full shrink-0"></div>
|
||||
<span className="text-muted-foreground">{item}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-green-600 mb-3">1.2 Usage Information</h3>
|
||||
<p className="text-muted-foreground mb-3">
|
||||
We may collect information about how you use our services, including:
|
||||
</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||
{informationTypes.usage.map((item, index) => (
|
||||
<div key={index} className="flex items-center space-x-3">
|
||||
<div className="w-2 h-2 bg-blue-600 rounded-full shrink-0"></div>
|
||||
<span className="text-muted-foreground">{item}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-green-600 mb-3">1.3 Cookies and Similar Technologies</h3>
|
||||
<p className="text-muted-foreground leading-relaxed">
|
||||
We use cookies and similar tracking technologies to track activity on our website and to hold certain information.
|
||||
Cookies are files with a small amount of data that may include an anonymous unique identifier. You can instruct your
|
||||
browser to refuse all cookies or to indicate when a cookie is being sent.
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* How We Use Your Information */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl flex items-center">
|
||||
<Eye className="w-6 h-6 mr-2 text-purple-600" />
|
||||
How We Use Your Information
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-muted-foreground mb-4">
|
||||
We may use the information we collect for various purposes, including:
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
{dataUses.map((use, index) => (
|
||||
<div key={index} className="flex items-start space-x-3">
|
||||
<div className="w-2 h-2 bg-purple-600 rounded-full mt-2 shrink-0"></div>
|
||||
<span className="text-muted-foreground">{use}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Information Sharing and Disclosure */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl flex items-center">
|
||||
<Users className="w-6 h-6 mr-2 text-orange-600" />
|
||||
Information Sharing and Disclosure
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<p className="text-muted-foreground">
|
||||
We may share your information in the following situations:
|
||||
</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{sharingScenarios.map((scenario, index) => (
|
||||
<div key={index} className="p-4 border rounded-lg">
|
||||
<h4 className="font-semibold text-orange-600 mb-2">{scenario.title}</h4>
|
||||
<p className="text-sm text-muted-foreground">{scenario.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-6 p-4 bg-green-50 dark:bg-green-950/10 rounded-lg border border-green-200 dark:border-green-800">
|
||||
<p className="text-sm text-green-800 dark:text-green-200 font-medium">
|
||||
We do not sell, rent, or trade your personal information with third parties for their commercial purposes.
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Data Security */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl flex items-center">
|
||||
<Shield className="w-6 h-6 mr-2 text-green-600" />
|
||||
Data Security
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-muted-foreground leading-relaxed">
|
||||
We implement appropriate technical and organizational measures to protect your personal information. However,
|
||||
no method of transmission over the internet is 100% secure. While we strive to use commercially acceptable
|
||||
means to protect your personal information, we cannot guarantee its absolute security.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Data Retention */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl">Data Retention</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-muted-foreground leading-relaxed">
|
||||
We will retain your personal information only for as long as is necessary for the purposes outlined in this
|
||||
Privacy Policy. We will retain and use your information to the extent necessary to comply with our legal
|
||||
obligations, resolve disputes, and enforce our policies.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Your Rights */}
|
||||
<Card id="profile-delete">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl flex items-center">
|
||||
<FileText className="w-6 h-6 mr-2 text-blue-600" />
|
||||
Your Rights
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<p className="text-muted-foreground">
|
||||
You may have certain rights regarding your personal information, such as:
|
||||
</p>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||
{userRights.map((right, index) => (
|
||||
<div key={index} className="flex items-center space-x-2 p-3 bg-muted/50 rounded-lg">
|
||||
<div className="w-2 h-2 bg-blue-600 rounded-full shrink-0"></div>
|
||||
<span className="text-sm font-medium">{right}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-muted-foreground mt-4">
|
||||
To exercise these rights, please contact us using the information provided at the end of this policy.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Children's Privacy */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl">Children's Privacy</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-muted-foreground leading-relaxed">
|
||||
Our services are not intended for children under 16. We do not knowingly collect personal information from
|
||||
children under 16. If you are a parent or guardian and you are aware that your child has provided us with
|
||||
personal information, please contact us so that we will be able to take necessary actions.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Changes to This Privacy Policy */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl">Changes to This Privacy Policy</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-muted-foreground leading-relaxed">
|
||||
We may update our Privacy Policy periodically. We will notify you of any changes by posting the new Privacy Policy
|
||||
on this page and updating the "Last Updated" date. You are advised to review this Privacy Policy periodically for
|
||||
any changes. Changes to this Privacy Policy are effective when they are posted on this page.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Contact Information */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl">Contact Us</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-muted-foreground leading-relaxed mb-4">
|
||||
If you have any questions about this Privacy Policy, please contact us:
|
||||
</p>
|
||||
<div className="space-y-2 text-muted-foreground">
|
||||
<p>
|
||||
Email:{' '}
|
||||
<a href="mailto:privacy@siliconpin.com" className="text-green-600 hover:underline font-medium">
|
||||
privacy@siliconpin.com
|
||||
</a>
|
||||
</p>
|
||||
<p>Phone: +91-700-160-1485</p>
|
||||
<p>
|
||||
Address: 121 Lalbari, GourBongo Road<br />
|
||||
Habra, West Bengal 743271<br />
|
||||
India
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* CTA Section */}
|
||||
<CustomSolutionCTA
|
||||
title="Questions About Your Privacy?"
|
||||
description="Our privacy team is here to help with any questions about how we handle your data and protect your privacy"
|
||||
buttonText="Contact Privacy Team"
|
||||
buttonHref="mailto:privacy@siliconpin.com"
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
182
app/profile/page.tsx
Normal file
182
app/profile/page.tsx
Normal file
@@ -0,0 +1,182 @@
|
||||
'use client'
|
||||
import { useSearchParams, useRouter } from 'next/navigation'
|
||||
import { useEffect, useState, useMemo } from 'react'
|
||||
import { RequireAuth } from '@/components/auth/RequireAuth'
|
||||
import { ProfileCard } from '@/components/profile/ProfileCard'
|
||||
import { ProfileSettings } from '@/components/profile/ProfileSettings'
|
||||
import { Header } from '@/components/header'
|
||||
import { Footer } from '@/components/footer'
|
||||
|
||||
function ProfileContent() {
|
||||
const searchParams = useSearchParams()
|
||||
const router = useRouter()
|
||||
const [activeTab, setActiveTab] = useState('profile')
|
||||
const [paymentResult, setPaymentResult] = useState<{
|
||||
type: 'success' | 'failed'
|
||||
message: string
|
||||
amount?: string
|
||||
txn?: string
|
||||
paymentType?: string
|
||||
} | null>(null)
|
||||
|
||||
const validTabs = useMemo(() => ['profile', 'balance', 'billing', 'services', 'security', 'danger'], [])
|
||||
|
||||
useEffect(() => {
|
||||
const tab = searchParams.get('tab')
|
||||
if (tab && validTabs.includes(tab)) {
|
||||
setActiveTab(tab)
|
||||
} else if (tab && !validTabs.includes(tab)) {
|
||||
// Handle invalid tab parameter by redirecting to default
|
||||
const params = new URLSearchParams(searchParams.toString())
|
||||
params.delete('tab')
|
||||
router.replace(`/profile${params.toString() ? `?${params.toString()}` : ''}`)
|
||||
}
|
||||
|
||||
// Handle payment result notifications
|
||||
const payment = searchParams.get('payment')
|
||||
const paymentType = searchParams.get('type')
|
||||
const amount = searchParams.get('amount')
|
||||
const txn = searchParams.get('txn')
|
||||
const reason = searchParams.get('reason')
|
||||
const message = searchParams.get('message')
|
||||
|
||||
if (payment === 'success') {
|
||||
setPaymentResult({
|
||||
type: 'success',
|
||||
message: paymentType === 'balance'
|
||||
? `Balance successfully added! ₹${amount ? parseFloat(amount).toLocaleString('en-IN') : '0'} has been credited to your account.`
|
||||
: `Payment successful! Your service payment has been processed.`,
|
||||
amount,
|
||||
txn,
|
||||
paymentType
|
||||
})
|
||||
|
||||
// If it's balance addition, switch to balance tab
|
||||
if (paymentType === 'balance') {
|
||||
setActiveTab('balance')
|
||||
}
|
||||
} else if (payment === 'failed') {
|
||||
const failureMessages: Record<string, string> = {
|
||||
'insufficient-funds': 'Payment failed due to insufficient funds.',
|
||||
'card-declined': 'Your card was declined. Please try a different payment method.',
|
||||
'timeout': 'Payment timed out. Please try again.',
|
||||
'user-cancelled': 'Payment was cancelled.',
|
||||
'invalid-details': 'Invalid payment details. Please check and try again.',
|
||||
'unknown': 'Payment failed due to an unexpected error.'
|
||||
}
|
||||
|
||||
setPaymentResult({
|
||||
type: 'failed',
|
||||
message: message ? decodeURIComponent(message) : (failureMessages[reason || 'unknown'] || 'Payment failed. Please try again.'),
|
||||
amount,
|
||||
txn,
|
||||
paymentType
|
||||
})
|
||||
}
|
||||
|
||||
// Clean up URL parameters after handling payment result
|
||||
if (payment) {
|
||||
setTimeout(() => {
|
||||
const params = new URLSearchParams(searchParams.toString())
|
||||
params.delete('payment')
|
||||
params.delete('type')
|
||||
params.delete('amount')
|
||||
params.delete('txn')
|
||||
params.delete('reason')
|
||||
params.delete('message')
|
||||
params.delete('warning')
|
||||
|
||||
const queryString = params.toString()
|
||||
router.replace(`/profile${queryString ? `?${queryString}` : ''}`, { scroll: false })
|
||||
}, 100) // Small delay to ensure state is set
|
||||
}
|
||||
}, [searchParams, router, validTabs])
|
||||
|
||||
const handleTabChange = (tab: string) => {
|
||||
if (!validTabs.includes(tab)) return
|
||||
|
||||
setActiveTab(tab)
|
||||
const params = new URLSearchParams(searchParams.toString())
|
||||
|
||||
if (tab === 'profile') {
|
||||
// Remove tab parameter for default tab to keep URLs clean
|
||||
params.delete('tab')
|
||||
} else {
|
||||
params.set('tab', tab)
|
||||
}
|
||||
|
||||
const queryString = params.toString()
|
||||
router.push(`/profile${queryString ? `?${queryString}` : ''}`)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container max-w-4xl pt-24 pb-8">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold">Profile</h1>
|
||||
<p className="text-muted-foreground">Manage your account settings and preferences</p>
|
||||
</div>
|
||||
|
||||
{/* Payment Result Notification */}
|
||||
{paymentResult && (
|
||||
<div className={`mb-6 p-4 rounded-lg border ${
|
||||
paymentResult.type === 'success'
|
||||
? 'bg-green-50 border-green-200 text-green-800 dark:bg-green-900/10 dark:border-green-700 dark:text-green-200'
|
||||
: 'bg-red-50 border-red-200 text-red-800 dark:bg-red-900/10 dark:border-red-700 dark:text-red-200'
|
||||
}`}>
|
||||
<div className="flex items-start">
|
||||
<div className={`mr-3 ${
|
||||
paymentResult.type === 'success' ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'
|
||||
}`}>
|
||||
{paymentResult.type === 'success' ? (
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="font-medium">{paymentResult.message}</p>
|
||||
{paymentResult.txn && (
|
||||
<p className="mt-1 text-sm opacity-75">
|
||||
Transaction ID: <code className="font-mono">{paymentResult.txn}</code>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setPaymentResult(null)}
|
||||
className="ml-3 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-1 lg:grid-cols-3">
|
||||
<div className="lg:col-span-1">
|
||||
<ProfileCard />
|
||||
</div>
|
||||
<div className="lg:col-span-2">
|
||||
<ProfileSettings activeTab={activeTab} onTabChange={handleTabChange} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function ProfilePage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<Header />
|
||||
<RequireAuth>
|
||||
<ProfileContent />
|
||||
</RequireAuth>
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
260
app/refund-policy/page.tsx
Normal file
260
app/refund-policy/page.tsx
Normal file
@@ -0,0 +1,260 @@
|
||||
import { Metadata } from 'next'
|
||||
import { generateMetadata } from '@/lib/seo'
|
||||
import { Header } from '@/components/header'
|
||||
import { Footer } from '@/components/footer'
|
||||
import { CustomSolutionCTA } from '@/components/ui/custom-solution-cta'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { CreditCard, Clock, Ban, RefreshCcw, MessageSquare, CheckCircle } from 'lucide-react'
|
||||
|
||||
export const metadata: Metadata = generateMetadata({
|
||||
title: 'Refund Policy - SiliconPin',
|
||||
description:
|
||||
'Learn about SiliconPin\'s refund policy for our hosting services. Find information about eligibility, process, and timeline for refunds.',
|
||||
})
|
||||
|
||||
export default function RefundPolicyPage() {
|
||||
const lastUpdated = "March 18, 2025"
|
||||
|
||||
const refundEligibility = [
|
||||
{
|
||||
title: '1.1 Service Cancellation within 30 Days',
|
||||
description: 'If you are dissatisfied with our services for any reason, you may request a refund within 30 days of the initial purchase date. This is our "30-day satisfaction guarantee" and applies to first-time purchases of hosting plans.',
|
||||
icon: Clock,
|
||||
color: 'text-green-600'
|
||||
},
|
||||
{
|
||||
title: '1.2 Service Unavailability',
|
||||
description: 'If our service experiences extended downtime (exceeding our 99.9% uptime guarantee) within a billing period, you may be eligible for a prorated refund for the affected period. This will be calculated based on the duration of the downtime and the cost of your hosting plan.',
|
||||
icon: RefreshCcw,
|
||||
color: 'text-orange-600'
|
||||
},
|
||||
{
|
||||
title: '1.3 Service Termination by SiliconPin',
|
||||
description: 'If SiliconPin terminates your service for reasons other than violation of our Terms and Conditions, you may be eligible for a prorated refund for the unused portion of your current billing period.',
|
||||
icon: CheckCircle,
|
||||
color: 'text-blue-600'
|
||||
}
|
||||
]
|
||||
|
||||
const nonRefundableItems = [
|
||||
'Domain registration fees',
|
||||
'Setup fees',
|
||||
'Additional services or add-ons',
|
||||
'Services cancelled after the 30-day satisfaction guarantee period',
|
||||
'Services terminated due to violation of our Terms and Conditions',
|
||||
'Transaction fees or currency conversion charges'
|
||||
]
|
||||
|
||||
const contactMethods = [
|
||||
{
|
||||
method: 'Email',
|
||||
value: 'support@siliconpin.com',
|
||||
href: 'mailto:support@siliconpin.com'
|
||||
},
|
||||
{
|
||||
method: 'Phone',
|
||||
value: '+91-700-160-1485',
|
||||
href: 'tel:+917001601485'
|
||||
},
|
||||
{
|
||||
method: 'Contact Form',
|
||||
value: 'Website contact form',
|
||||
href: '/contact'
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<Header />
|
||||
<main className="container max-w-4xl pt-24 pb-8">
|
||||
{/* Page Header */}
|
||||
<div className="text-center mb-12">
|
||||
<h1 className="text-4xl font-bold tracking-tight mb-4">Refund Policy</h1>
|
||||
<p className="text-xl text-muted-foreground max-w-3xl mx-auto">
|
||||
Our commitment to fair and transparent refund procedures
|
||||
</p>
|
||||
<div className="mt-4">
|
||||
<Badge variant="outline" className="text-sm">
|
||||
<CreditCard className="w-4 h-4 mr-2" />
|
||||
Last Updated: {lastUpdated}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-8">
|
||||
{/* Introduction */}
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<p className="text-muted-foreground leading-relaxed">
|
||||
At SiliconPin, we strive to provide high-quality hosting services that meet our customers' expectations.
|
||||
We understand that there may be circumstances where a refund is warranted, and we have established this
|
||||
refund policy to outline the conditions and procedures for such situations.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Eligibility for Refunds */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl flex items-center">
|
||||
<CheckCircle className="w-6 h-6 mr-2 text-green-600" />
|
||||
Eligibility for Refunds
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
{refundEligibility.map((item, index) => {
|
||||
const Icon = item.icon
|
||||
return (
|
||||
<div key={index} className="space-y-3">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="p-2 rounded-lg bg-primary/10">
|
||||
<Icon className={`w-5 h-5 ${item.color}`} />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-green-600">{item.title}</h3>
|
||||
</div>
|
||||
<p className="text-muted-foreground leading-relaxed pl-11">
|
||||
{item.description}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Non-Refundable Items */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl flex items-center">
|
||||
<Ban className="w-6 h-6 mr-2 text-red-600" />
|
||||
Non-Refundable Items
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-muted-foreground mb-4">
|
||||
The following items and circumstances are generally not eligible for refunds:
|
||||
</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{nonRefundableItems.map((item, index) => (
|
||||
<div key={index} className="flex items-start space-x-3">
|
||||
<div className="w-2 h-2 bg-red-600 rounded-full mt-2 shrink-0"></div>
|
||||
<span className="text-muted-foreground">{item}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Refund Process */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl flex items-center">
|
||||
<RefreshCcw className="w-6 h-6 mr-2 text-blue-600" />
|
||||
Refund Process
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-green-600 mb-3">3.1 Requesting a Refund</h3>
|
||||
<p className="text-muted-foreground mb-4">
|
||||
To request a refund, please contact our customer support team through one of the following methods:
|
||||
</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{contactMethods.map((contact, index) => (
|
||||
<div key={index} className="p-4 border rounded-lg hover:border-green-600 transition-colors">
|
||||
<div className="font-medium mb-2">{contact.method}</div>
|
||||
<a
|
||||
href={contact.href}
|
||||
className="text-green-600 hover:underline text-sm"
|
||||
>
|
||||
{contact.value}
|
||||
</a>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-muted-foreground mt-4">
|
||||
Please include your account information, the service you wish to cancel, and the reason for your refund request.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-green-600 mb-3">3.2 Processing Time</h3>
|
||||
<div className="flex items-start space-x-3 p-4 bg-blue-50 dark:bg-blue-950/10 rounded-lg border border-blue-200 dark:border-blue-800">
|
||||
<Clock className="w-5 h-5 text-blue-600 mt-0.5" />
|
||||
<div>
|
||||
<p className="text-blue-800 dark:text-blue-200 font-medium mb-1">
|
||||
7 Business Days Processing
|
||||
</p>
|
||||
<p className="text-sm text-blue-700 dark:text-blue-300">
|
||||
We aim to process all refund requests within 7 business days of receiving the request.
|
||||
The actual time for the refunded amount to appear in your account may vary depending on your payment provider.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-green-600 mb-3">3.3 Refund Method</h3>
|
||||
<p className="text-muted-foreground leading-relaxed">
|
||||
Refunds will be issued using the same payment method used for the original purchase.
|
||||
If this is not possible, we will work with you to find an alternative method.
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Policy Modifications */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl">Policy Modifications</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-muted-foreground leading-relaxed">
|
||||
SiliconPin reserves the right to modify this refund policy at any time. Any changes to this policy
|
||||
will be posted on our website and will apply to purchases made after the date of modification.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Contact Information */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl flex items-center">
|
||||
<MessageSquare className="w-6 h-6 mr-2 text-purple-600" />
|
||||
Contact Us
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-muted-foreground leading-relaxed mb-4">
|
||||
If you have any questions or concerns about our refund policy, please don't hesitate to contact us:
|
||||
</p>
|
||||
<div className="space-y-2 text-muted-foreground">
|
||||
<p>
|
||||
Email:{' '}
|
||||
<a href="mailto:support@siliconpin.com" className="text-green-600 hover:underline font-medium">
|
||||
support@siliconpin.com
|
||||
</a>
|
||||
</p>
|
||||
<p>Phone: +91-700-160-1485</p>
|
||||
<p>
|
||||
Address: 121 Lalbari, GourBongo Road<br />
|
||||
Habra, West Bengal 743271<br />
|
||||
India
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* CTA Section */}
|
||||
<CustomSolutionCTA
|
||||
title="Need Help with a Refund?"
|
||||
description="Our support team is here to assist you with any refund requests or questions about our policy"
|
||||
buttonText="Contact Support"
|
||||
buttonHref="/contact"
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
47
app/robots.ts
Normal file
47
app/robots.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { MetadataRoute } from 'next'
|
||||
|
||||
export default function robots(): MetadataRoute.Robots {
|
||||
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:4023'
|
||||
|
||||
return {
|
||||
rules: [
|
||||
{
|
||||
userAgent: '*',
|
||||
allow: [
|
||||
'/',
|
||||
'/about',
|
||||
'/services',
|
||||
'/tools',
|
||||
'/topics',
|
||||
'/contact',
|
||||
'/feedback',
|
||||
'/terms',
|
||||
'/privacy',
|
||||
'/legal-agreement',
|
||||
'/refund-policy',
|
||||
],
|
||||
disallow: [
|
||||
'/api/',
|
||||
'/admin/',
|
||||
'/_next/',
|
||||
'/private/',
|
||||
'/profile',
|
||||
'/auth',
|
||||
'/*.tmp$',
|
||||
'/*.bak$',
|
||||
'/*.json$',
|
||||
],
|
||||
},
|
||||
{
|
||||
userAgent: 'GPTBot',
|
||||
disallow: '/',
|
||||
},
|
||||
{
|
||||
userAgent: 'Google-Extended',
|
||||
disallow: '/',
|
||||
},
|
||||
],
|
||||
sitemap: `${baseUrl}/sitemap.xml`,
|
||||
host: baseUrl,
|
||||
}
|
||||
}
|
||||
719
app/services/cloud-instance/page.tsx
Normal file
719
app/services/cloud-instance/page.tsx
Normal file
@@ -0,0 +1,719 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { Header } from '@/components/header'
|
||||
import { Footer } from '@/components/footer'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import {
|
||||
Cloud,
|
||||
Server,
|
||||
HardDrive,
|
||||
Shield,
|
||||
AlertCircle,
|
||||
Check,
|
||||
Cpu,
|
||||
MapPin,
|
||||
ExternalLink,
|
||||
Terminal,
|
||||
} from 'lucide-react'
|
||||
|
||||
import { useAuth } from '@/contexts/AuthContext'
|
||||
import { checkSufficientBalance, formatCurrency } from '@/lib/balance-service'
|
||||
|
||||
interface Plan {
|
||||
id: string
|
||||
name: string
|
||||
price: {
|
||||
daily: number
|
||||
monthly: number
|
||||
}
|
||||
}
|
||||
|
||||
interface OSImage {
|
||||
id: string
|
||||
name: string
|
||||
price?: {
|
||||
daily: number
|
||||
monthly: number
|
||||
}
|
||||
}
|
||||
|
||||
interface DataCenter {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
|
||||
interface DeploymentResult {
|
||||
status: string
|
||||
cloudid?: string
|
||||
message?: string
|
||||
password?: string
|
||||
ipv4?: string
|
||||
server_id?: string
|
||||
error?: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export default function CloudInstancePage() {
|
||||
const router = useRouter()
|
||||
const { user } = useAuth()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const [success, setSuccess] = useState('')
|
||||
const [deploymentResult, setDeploymentResult] = useState<DeploymentResult | null>(null)
|
||||
const [billingCycle, setBillingCycle] = useState<'daily' | 'monthly'>('monthly')
|
||||
|
||||
// Form state
|
||||
const [formData, setFormData] = useState({
|
||||
hostname: '',
|
||||
planId: '',
|
||||
image: '',
|
||||
password: '',
|
||||
dcslug: '',
|
||||
enableBackup: false,
|
||||
enablePublicIp: true,
|
||||
})
|
||||
|
||||
// Available plans
|
||||
const plans: Plan[] = [
|
||||
{ id: '10027', name: '2 vCPU, 4GB RAM, 80GB SSD', price: { daily: 60, monthly: 1600 } },
|
||||
{ id: '10028', name: '4 vCPU, 8GB RAM, 160GB SSD', price: { daily: 80, monthly: 2200 } },
|
||||
{ id: '10029', name: '8 vCPU, 32GB RAM, 480GB SSD', price: { daily: 100, monthly: 2800 } },
|
||||
{ id: '10030', name: '16 vCPU, 64GB RAM, 960GB SSD', price: { daily: 120, monthly: 3200 } },
|
||||
]
|
||||
|
||||
// Available OS images
|
||||
const images: OSImage[] = [
|
||||
{ id: 'almalinux-9.2-x86_64', name: 'Alma Linux 9.2' },
|
||||
{ id: 'centos-7.9-x86_64', name: 'CentOS 7.9' },
|
||||
{ id: 'debian-12-x86_64', name: 'Debian 12' },
|
||||
{ id: 'ubuntu-22.04-x86_64', name: 'Ubuntu 22.04 LTS' },
|
||||
{ id: 'windows-2022', name: 'Windows Server 2022', price: { daily: 200, monthly: 5000 } },
|
||||
]
|
||||
|
||||
// Data centers
|
||||
const dataCenters: DataCenter[] = [
|
||||
{ id: 'inmumbaizone2', name: 'Mumbai, India' },
|
||||
{ id: 'inbangalore', name: 'Bangalore, India' },
|
||||
]
|
||||
|
||||
// Get user balance from auth context
|
||||
const userBalance = user?.balance || 0
|
||||
|
||||
// Calculate total price
|
||||
const calculatePrice = () => {
|
||||
const selectedPlan = plans.find((p) => p.id === formData.planId)
|
||||
const selectedImage = images.find((i) => i.id === formData.image)
|
||||
|
||||
if (!selectedPlan) return 0
|
||||
|
||||
let total = selectedPlan.price[billingCycle]
|
||||
|
||||
// Add Windows license cost if applicable
|
||||
if (selectedImage?.price) {
|
||||
total += selectedImage.price[billingCycle]
|
||||
}
|
||||
|
||||
// Add backup cost (20% of plan price)
|
||||
if (formData.enableBackup) {
|
||||
total += selectedPlan.price[billingCycle] * 0.2
|
||||
}
|
||||
|
||||
return total
|
||||
}
|
||||
|
||||
const handleDeploy = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setError('')
|
||||
setSuccess('')
|
||||
setDeploymentResult(null)
|
||||
|
||||
if (!user) {
|
||||
router.push('/auth?redirect=/services/cloud-instance')
|
||||
return
|
||||
}
|
||||
|
||||
// Validate form
|
||||
if (
|
||||
!formData.hostname ||
|
||||
!formData.planId ||
|
||||
!formData.image ||
|
||||
!formData.password ||
|
||||
!formData.dcslug
|
||||
) {
|
||||
setError('All fields are required')
|
||||
return
|
||||
}
|
||||
|
||||
// Check password strength
|
||||
if (formData.password.length < 8) {
|
||||
setError('Password must be at least 8 characters long')
|
||||
return
|
||||
}
|
||||
|
||||
// Check balance
|
||||
const totalPrice = calculatePrice()
|
||||
const balanceCheck = await checkSufficientBalance(totalPrice)
|
||||
|
||||
if (!balanceCheck.sufficient) {
|
||||
setError(
|
||||
balanceCheck.error ||
|
||||
`Insufficient balance. You need ${formatCurrency(totalPrice)} but only have ${formatCurrency(balanceCheck.currentBalance || 0)}`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/services/deploy-cloude', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
hostname: formData.hostname,
|
||||
planid: formData.planId,
|
||||
image: formData.image,
|
||||
dclocation: formData.dcslug,
|
||||
password: formData.password,
|
||||
cycle: billingCycle,
|
||||
amount: totalPrice,
|
||||
backup: formData.enableBackup,
|
||||
publicip: formData.enablePublicIp,
|
||||
}),
|
||||
})
|
||||
|
||||
const result: DeploymentResult = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(result.message || `Deployment failed with status ${response.status}`)
|
||||
}
|
||||
|
||||
if (result.status === 'success') {
|
||||
setDeploymentResult(result)
|
||||
|
||||
// Create success message with server details
|
||||
let successMessage = 'Cloud instance deployed successfully!'
|
||||
if (result.cloudid) {
|
||||
successMessage += ` Server ID: ${result.cloudid}`
|
||||
}
|
||||
if (result.ipv4) {
|
||||
successMessage += ` | IP: ${result.ipv4}`
|
||||
}
|
||||
|
||||
setSuccess(successMessage)
|
||||
|
||||
// Reset form after successful deployment
|
||||
setFormData({
|
||||
hostname: '',
|
||||
planId: '',
|
||||
image: '',
|
||||
password: '',
|
||||
dcslug: '',
|
||||
enableBackup: false,
|
||||
enablePublicIp: true,
|
||||
})
|
||||
} else {
|
||||
throw new Error(result.message || 'Deployment failed')
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Deployment error:', err)
|
||||
setError(err instanceof Error ? err.message : 'Failed to deploy cloud instance')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
setDeploymentResult(null)
|
||||
setSuccess('')
|
||||
setError('')
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<Header />
|
||||
|
||||
<div className="container mx-auto px-4 pt-24 pb-16">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
{/* Page Header */}
|
||||
<div className="text-center mb-12">
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 bg-gradient-to-br from-blue-500 to-purple-600 rounded-full mb-4">
|
||||
<Cloud className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
<h1 className="text-4xl font-bold mb-4">Deploy Cloud Instance</h1>
|
||||
<p className="text-xl text-muted-foreground">
|
||||
High-performance cloud servers with instant deployment
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{!user && (
|
||||
<Alert className="mb-8">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
You need to be logged in to deploy a cloud instance.{' '}
|
||||
<a
|
||||
href="/auth?redirect=/services/cloud-instance"
|
||||
className="text-primary underline"
|
||||
>
|
||||
Click here to login
|
||||
</a>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{success && (
|
||||
<Alert className="mb-8 bg-green-50 border-green-200">
|
||||
<Check className="h-4 w-4 text-green-600" />
|
||||
<AlertDescription className="text-green-800">
|
||||
<div className="space-y-3">
|
||||
<div className="font-semibold">{success}</div>
|
||||
|
||||
{deploymentResult?.ipv4 && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-3">
|
||||
<div>
|
||||
<Label className="text-sm font-medium text-green-700">
|
||||
Server IP Address
|
||||
</Label>
|
||||
<div className="flex items-center mt-1">
|
||||
<Input
|
||||
className="font-mono text-sm bg-green-100 border-green-300"
|
||||
value={deploymentResult.ipv4}
|
||||
readOnly
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="ml-2"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(deploymentResult.ipv4!)
|
||||
// You can add a toast notification here
|
||||
}}
|
||||
>
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-sm font-medium text-green-700">Server ID</Label>
|
||||
<p className="text-sm font-mono bg-green-100 p-2 rounded border border-green-300 mt-1">
|
||||
{deploymentResult.cloudid || deploymentResult.server_id}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{deploymentResult?.password && (
|
||||
<div className="mt-3">
|
||||
<Label className="text-sm font-medium text-green-700">Root Password</Label>
|
||||
<div className="flex items-center mt-1">
|
||||
<Input
|
||||
type="password"
|
||||
className="font-mono text-sm bg-green-100 border-green-300"
|
||||
value={deploymentResult.password}
|
||||
readOnly
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="ml-2"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(deploymentResult.password!)
|
||||
// You can add a toast notification here
|
||||
}}
|
||||
>
|
||||
Copy
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-green-600 mt-1">
|
||||
Please change this password after first login
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{deploymentResult?.message && (
|
||||
<div className="mt-3 p-3 bg-green-100 rounded-md">
|
||||
<p className="text-sm text-green-700">{deploymentResult.message}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-3 mt-4 pt-3 border-t border-green-200">
|
||||
<Button
|
||||
onClick={() => router.push('/dashboard/instances')}
|
||||
className="bg-green-600 hover:bg-green-700"
|
||||
>
|
||||
<Server className="w-4 h-4 mr-2" />
|
||||
View All Instances
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
// SSH connection button - you can implement this
|
||||
if (deploymentResult?.ipv4) {
|
||||
const sshCommand = `ssh root@${deploymentResult.ipv4}`
|
||||
navigator.clipboard.writeText(sshCommand)
|
||||
// Show toast notification
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Terminal className="w-4 h-4 mr-2" />
|
||||
Copy SSH Command
|
||||
</Button>
|
||||
|
||||
<Button variant="outline" onClick={resetForm}>
|
||||
Deploy Another
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 text-xs text-green-600">
|
||||
<p>✅ Deployment successful! Your server is being provisioned.</p>
|
||||
{/* <p>📧 Login details will be sent to your email once ready.</p> */}
|
||||
</div>
|
||||
</div>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{deploymentResult ? (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Deployment Successful!</CardTitle>
|
||||
<CardDescription>
|
||||
Your cloud instance has been deployed successfully.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label>Server ID</Label>
|
||||
<p className="text-sm font-mono bg-muted p-2 rounded">
|
||||
{deploymentResult.server_id}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Status</Label>
|
||||
<p className="text-sm capitalize">{deploymentResult.status}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4">
|
||||
<Button onClick={() => router.push('/dashboard/instances')}>
|
||||
View Instances
|
||||
</Button>
|
||||
<Button variant="outline" onClick={resetForm}>
|
||||
Deploy Another
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<form onSubmit={handleDeploy}>
|
||||
<div className="space-y-8">
|
||||
{/* Billing Cycle Selection */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Billing Cycle</CardTitle>
|
||||
<CardDescription>Choose your preferred billing period</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Tabs
|
||||
value={billingCycle}
|
||||
onValueChange={(v) => setBillingCycle(v as 'daily' | 'monthly')}
|
||||
>
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="daily">Daily</TabsTrigger>
|
||||
<TabsTrigger value="monthly">Monthly (Save 20%)</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Server Configuration */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Server Configuration</CardTitle>
|
||||
<CardDescription>Configure your cloud instance specifications</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="hostname">Hostname</Label>
|
||||
<Input
|
||||
id="hostname"
|
||||
placeholder="my-server.example.com"
|
||||
value={formData.hostname}
|
||||
onChange={(e) => setFormData({ ...formData, hostname: e.target.value })}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="plan">Server Plan</Label>
|
||||
<Select
|
||||
value={formData.planId}
|
||||
onValueChange={(value) => setFormData({ ...formData, planId: value })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a plan" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{plans.map((plan) => (
|
||||
<SelectItem key={plan.id} value={plan.id}>
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<span>{plan.name}</span>
|
||||
<span className="ml-4 text-primary">
|
||||
₹{plan.price[billingCycle]}/
|
||||
{billingCycle === 'daily' ? 'day' : 'month'}
|
||||
</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="image">Operating System</Label>
|
||||
<Select
|
||||
value={formData.image}
|
||||
onValueChange={(value) => setFormData({ ...formData, image: value })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select an OS" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{images.map((image) => (
|
||||
<SelectItem key={image.id} value={image.id}>
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<span>{image.name}</span>
|
||||
{image.price && (
|
||||
<span className="ml-4 text-primary">
|
||||
+₹{image.price[billingCycle]}/
|
||||
{billingCycle === 'daily' ? 'day' : 'month'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="datacenter">Data Center</Label>
|
||||
<Select
|
||||
value={formData.dcslug}
|
||||
onValueChange={(value) => setFormData({ ...formData, dcslug: value })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select location" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{dataCenters.map((dc) => (
|
||||
<SelectItem key={dc.id} value={dc.id}>
|
||||
<div className="flex items-center gap-2">
|
||||
<MapPin className="w-4 h-4" />
|
||||
{dc.name}
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="password">Root Password</Label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
placeholder="Strong password (min 8 characters)"
|
||||
value={formData.password}
|
||||
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
|
||||
required
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Minimum 8 characters required
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="backup"
|
||||
checked={formData.enableBackup}
|
||||
onCheckedChange={(checked) =>
|
||||
setFormData({ ...formData, enableBackup: checked as boolean })
|
||||
}
|
||||
/>
|
||||
<Label htmlFor="backup" className="cursor-pointer">
|
||||
Enable automatic backups (+20% of plan cost)
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="publicip"
|
||||
checked={formData.enablePublicIp}
|
||||
onCheckedChange={(checked) =>
|
||||
setFormData({ ...formData, enablePublicIp: checked as boolean })
|
||||
}
|
||||
/>
|
||||
<Label htmlFor="publicip" className="cursor-pointer">
|
||||
Enable public IP address
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Pricing Summary */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Order Summary</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Base Plan</span>
|
||||
<span>
|
||||
₹{plans.find((p) => p.id === formData.planId)?.price[billingCycle] || 0}
|
||||
</span>
|
||||
</div>
|
||||
{images.find((i) => i.id === formData.image)?.price && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Windows License</span>
|
||||
<span>
|
||||
₹
|
||||
{images.find((i) => i.id === formData.image)?.price?.[billingCycle] ||
|
||||
0}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{formData.enableBackup && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Backup Service</span>
|
||||
<span>
|
||||
₹
|
||||
{(plans.find((p) => p.id === formData.planId)?.price[billingCycle] ||
|
||||
0) * 0.2}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="border-t pt-2">
|
||||
<div className="flex justify-between font-semibold">
|
||||
<span>Total</span>
|
||||
<span className="text-primary">
|
||||
₹{calculatePrice()}/{billingCycle === 'daily' ? 'day' : 'month'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pt-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground">Your Balance</span>
|
||||
<span
|
||||
className={
|
||||
userBalance >= calculatePrice() ? 'text-green-600' : 'text-red-600'
|
||||
}
|
||||
>
|
||||
₹{userBalance.toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{error && (
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Deploy Button */}
|
||||
<Button type="submit" className="w-full" size="lg" disabled={loading || !user}>
|
||||
{loading ? (
|
||||
<>
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
||||
Deploying...
|
||||
</>
|
||||
) : (
|
||||
'Deploy Cloud Instance'
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
|
||||
{/* Features */}
|
||||
{!deploymentResult && (
|
||||
<div className="mt-16 grid md:grid-cols-3 gap-8">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Server className="w-8 h-8 text-primary mb-2" />
|
||||
<CardTitle>High Performance</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Enterprise-grade hardware with NVMe SSD storage
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Shield className="w-8 h-8 text-primary mb-2" />
|
||||
<CardTitle>Secure & Reliable</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
DDoS protection and 99.99% uptime SLA
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<HardDrive className="w-8 h-8 text-primary mb-2" />
|
||||
<CardTitle>Instant Deployment</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Your server ready in less than 60 seconds
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// jRIfuQspDJlWdgXxLmqVFSUthwBOkezibPCyoTvHYcEANraGKMnZ
|
||||
// {
|
||||
// "status": "success",
|
||||
// "cloudid": "1645972",
|
||||
// "message": "Cloud Server deploy in process and as soon it get ready to use system will send you login detail over the email.",
|
||||
// "password": "00000000",
|
||||
// "ipv4": "103.189.89.32"
|
||||
// }
|
||||
426
app/services/hosting-control-panel/page.tsx
Normal file
426
app/services/hosting-control-panel/page.tsx
Normal file
@@ -0,0 +1,426 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { Header } from '@/components/header'
|
||||
import { Footer } from '@/components/footer'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { Server, Shield, HardDrive, Globe, Mail, AlertCircle, Check, Settings } from 'lucide-react'
|
||||
import { useAuth } from '@/contexts/AuthContext'
|
||||
|
||||
interface HostingPlan {
|
||||
id: string
|
||||
controlPanel: string
|
||||
billingCycle: string[]
|
||||
price: {
|
||||
monthly: number
|
||||
yearly: number
|
||||
}
|
||||
storage: string
|
||||
bandwidth: string
|
||||
domainsAllowed: string
|
||||
ssl: boolean
|
||||
support: string
|
||||
popular?: boolean
|
||||
}
|
||||
|
||||
export default function HostingControlPanelPage() {
|
||||
const router = useRouter()
|
||||
const { user } = useAuth()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const [billingCycle, setBillingCycle] = useState<'monthly' | 'yearly'>('monthly')
|
||||
const [selectedPlan, setSelectedPlan] = useState('')
|
||||
const [domain, setDomain] = useState('')
|
||||
|
||||
// Available hosting plans
|
||||
const plans: HostingPlan[] = [
|
||||
{
|
||||
id: 'plan_001',
|
||||
controlPanel: 'cPanel',
|
||||
billingCycle: ['monthly', 'yearly'],
|
||||
price: { monthly: 500, yearly: 5000 },
|
||||
storage: '100 GB SSD',
|
||||
bandwidth: 'Unlimited',
|
||||
domainsAllowed: 'Unlimited',
|
||||
ssl: true,
|
||||
support: '24/7 Priority Support',
|
||||
popular: true,
|
||||
},
|
||||
{
|
||||
id: 'plan_002',
|
||||
controlPanel: 'Plesk',
|
||||
billingCycle: ['monthly', 'yearly'],
|
||||
price: { monthly: 450, yearly: 4500 },
|
||||
storage: '80 GB SSD',
|
||||
bandwidth: 'Unlimited',
|
||||
domainsAllowed: '50',
|
||||
ssl: true,
|
||||
support: '24/7 Support',
|
||||
},
|
||||
{
|
||||
id: 'plan_003',
|
||||
controlPanel: 'DirectAdmin',
|
||||
billingCycle: ['monthly', 'yearly'],
|
||||
price: { monthly: 350, yearly: 3500 },
|
||||
storage: '60 GB SSD',
|
||||
bandwidth: '2TB/month',
|
||||
domainsAllowed: '25',
|
||||
ssl: true,
|
||||
support: 'Email & Chat',
|
||||
},
|
||||
{
|
||||
id: 'plan_004',
|
||||
controlPanel: 'HestiaCP',
|
||||
billingCycle: ['monthly', 'yearly'],
|
||||
price: { monthly: 199, yearly: 1999 },
|
||||
storage: '50 GB SSD',
|
||||
bandwidth: 'Unlimited',
|
||||
domainsAllowed: 'Unlimited',
|
||||
ssl: true,
|
||||
support: 'Email & Chat',
|
||||
},
|
||||
{
|
||||
id: 'plan_005',
|
||||
controlPanel: 'Webmin',
|
||||
billingCycle: ['monthly', 'yearly'],
|
||||
price: { monthly: 149, yearly: 1499 },
|
||||
storage: '40 GB SSD',
|
||||
bandwidth: 'Unlimited',
|
||||
domainsAllowed: '10',
|
||||
ssl: true,
|
||||
support: 'Email Only',
|
||||
},
|
||||
{
|
||||
id: 'plan_006',
|
||||
controlPanel: 'VestaCP',
|
||||
billingCycle: ['monthly', 'yearly'],
|
||||
price: { monthly: 129, yearly: 1299 },
|
||||
storage: '30 GB SSD',
|
||||
bandwidth: '500 GB/month',
|
||||
domainsAllowed: '5',
|
||||
ssl: true,
|
||||
support: 'Email Only',
|
||||
},
|
||||
]
|
||||
|
||||
// Mock user balance
|
||||
const userBalance = 10000
|
||||
|
||||
const handlePurchase = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setError('')
|
||||
|
||||
if (!user) {
|
||||
router.push('/auth?redirect=/services/hosting-control-panel')
|
||||
return
|
||||
}
|
||||
|
||||
// Validate form
|
||||
if (!domain || !selectedPlan) {
|
||||
setError('Please fill in all required fields')
|
||||
return
|
||||
}
|
||||
|
||||
// Validate domain format
|
||||
const domainRegex = /^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9](?:\.[a-zA-Z]{2,})+$/
|
||||
if (!domainRegex.test(domain)) {
|
||||
setError('Please enter a valid domain name')
|
||||
return
|
||||
}
|
||||
|
||||
// Get selected plan price
|
||||
const plan = plans.find(p => p.id === selectedPlan)
|
||||
if (!plan) {
|
||||
setError('Please select a valid plan')
|
||||
return
|
||||
}
|
||||
|
||||
const totalPrice = plan.price[billingCycle]
|
||||
|
||||
// Check balance
|
||||
if (userBalance < totalPrice) {
|
||||
setError(`Insufficient balance. You need ₹${totalPrice.toFixed(2)} but only have ₹${userBalance.toFixed(2)}`)
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
|
||||
// Simulate API call
|
||||
setTimeout(() => {
|
||||
setLoading(false)
|
||||
alert('Hosting package purchased successfully! (This is a demo - no actual purchase)')
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<Header />
|
||||
|
||||
<div className="container mx-auto px-4 pt-24 pb-16">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
{/* Page Header */}
|
||||
<div className="text-center mb-12">
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 bg-gradient-to-br from-blue-500 to-purple-600 rounded-full mb-4">
|
||||
<Settings className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
<h1 className="text-4xl font-bold mb-4">Hosting Control Panels</h1>
|
||||
<p className="text-xl text-muted-foreground">
|
||||
Professional web hosting with your preferred control panel
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{!user && (
|
||||
<Alert className="mb-8">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
You need to be logged in to purchase hosting.{' '}
|
||||
<a href="/auth?redirect=/services/hosting-control-panel" className="text-primary underline">
|
||||
Click here to login
|
||||
</a>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<form onSubmit={handlePurchase}>
|
||||
<div className="space-y-8">
|
||||
{/* Billing Cycle Selection */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Billing Cycle</CardTitle>
|
||||
<CardDescription>Choose your preferred billing period</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Tabs value={billingCycle} onValueChange={(v) => setBillingCycle(v as 'monthly' | 'yearly')}>
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="monthly">Monthly</TabsTrigger>
|
||||
<TabsTrigger value="yearly">Yearly (Save 17%)</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Available Plans */}
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold mb-6">Choose Your Control Panel</h2>
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{plans.map((plan) => (
|
||||
<Card
|
||||
key={plan.id}
|
||||
className={`relative cursor-pointer transition-all ${
|
||||
selectedPlan === plan.id ? 'ring-2 ring-primary' : ''
|
||||
} ${plan.popular ? 'border-primary' : ''}`}
|
||||
onClick={() => setSelectedPlan(plan.id)}
|
||||
>
|
||||
{plan.popular && (
|
||||
<div className="absolute -top-3 left-1/2 transform -translate-x-1/2">
|
||||
<span className="bg-gradient-to-r from-blue-500 to-purple-600 text-white px-3 py-1 rounded-full text-xs font-semibold">
|
||||
Most Popular
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">{plan.controlPanel}</CardTitle>
|
||||
<div className="text-2xl font-bold text-primary">
|
||||
₹{plan.price[billingCycle]}
|
||||
<span className="text-sm font-normal text-muted-foreground">
|
||||
/{billingCycle === 'monthly' ? 'month' : 'year'}
|
||||
</span>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-3">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<HardDrive className="w-4 h-4 text-muted-foreground" />
|
||||
<span className="text-sm">{plan.storage}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Globe className="w-4 h-4 text-muted-foreground" />
|
||||
<span className="text-sm">{plan.bandwidth} Bandwidth</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Server className="w-4 h-4 text-muted-foreground" />
|
||||
<span className="text-sm">{plan.domainsAllowed} Domains</span>
|
||||
</div>
|
||||
{plan.ssl && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Shield className="w-4 h-4 text-green-500" />
|
||||
<span className="text-sm">Free SSL Certificate</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
<Mail className="w-4 h-4 text-muted-foreground" />
|
||||
<span className="text-sm">{plan.support}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<RadioGroup value={selectedPlan}>
|
||||
<div className="flex items-center justify-center pt-2">
|
||||
<RadioGroupItem value={plan.id} id={plan.id} />
|
||||
<Label htmlFor={plan.id} className="ml-2 cursor-pointer">
|
||||
Select this plan
|
||||
</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Domain Configuration */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Domain Configuration</CardTitle>
|
||||
<CardDescription>Enter the primary domain for your hosting account</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div>
|
||||
<Label htmlFor="domain">Primary Domain</Label>
|
||||
<Input
|
||||
id="domain"
|
||||
type="text"
|
||||
placeholder="example.com"
|
||||
value={domain}
|
||||
onChange={(e) => setDomain(e.target.value)}
|
||||
required
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
You can add more domains later from your control panel
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Order Summary */}
|
||||
{selectedPlan && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Order Summary</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Control Panel</span>
|
||||
<span>{plans.find(p => p.id === selectedPlan)?.controlPanel}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Billing Cycle</span>
|
||||
<span className="capitalize">{billingCycle}</span>
|
||||
</div>
|
||||
<div className="border-t pt-2">
|
||||
<div className="flex justify-between font-semibold">
|
||||
<span>Total</span>
|
||||
<span className="text-primary">
|
||||
₹{plans.find(p => p.id === selectedPlan)?.price[billingCycle]}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pt-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground">Your Balance</span>
|
||||
<span className={userBalance >= (plans.find(p => p.id === selectedPlan)?.price[billingCycle] || 0) ? 'text-green-600' : 'text-red-600'}>
|
||||
₹{userBalance.toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Purchase Button */}
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full"
|
||||
size="lg"
|
||||
disabled={loading || !user || !selectedPlan}
|
||||
>
|
||||
{loading ? 'Processing...' : 'Purchase Hosting Package'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{/* Features Grid */}
|
||||
<div className="mt-16">
|
||||
<h3 className="text-2xl font-semibold mb-8 text-center">All Plans Include</h3>
|
||||
<div className="grid md:grid-cols-4 gap-6">
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="text-center">
|
||||
<div className="inline-flex items-center justify-center w-12 h-12 bg-primary/10 rounded-full mb-3">
|
||||
<Check className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<h4 className="font-semibold mb-2">One-Click Apps</h4>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Install WordPress, Joomla, and more with one click
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="text-center">
|
||||
<div className="inline-flex items-center justify-center w-12 h-12 bg-primary/10 rounded-full mb-3">
|
||||
<Shield className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<h4 className="font-semibold mb-2">Free SSL</h4>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Secure your website with free SSL certificates
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="text-center">
|
||||
<div className="inline-flex items-center justify-center w-12 h-12 bg-primary/10 rounded-full mb-3">
|
||||
<Server className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<h4 className="font-semibold mb-2">Daily Backups</h4>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Automatic daily backups with easy restore
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="text-center">
|
||||
<div className="inline-flex items-center justify-center w-12 h-12 bg-primary/10 rounded-full mb-3">
|
||||
<Mail className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<h4 className="font-semibold mb-2">Email Accounts</h4>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Create professional email addresses
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
582
app/services/human-developer/page.tsx
Normal file
582
app/services/human-developer/page.tsx
Normal file
@@ -0,0 +1,582 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { Header } from '@/components/header'
|
||||
import { Footer } from '@/components/footer'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import {
|
||||
Clock,
|
||||
Calendar,
|
||||
CalendarDays,
|
||||
Users,
|
||||
Code,
|
||||
Briefcase,
|
||||
CheckCircle,
|
||||
AlertCircle,
|
||||
Star,
|
||||
} from 'lucide-react'
|
||||
import { useAuth } from '@/contexts/AuthContext'
|
||||
import { useToast } from '@/hooks/use-toast'
|
||||
import { formatCurrency, validateBalance } from '@/lib/balance-service'
|
||||
|
||||
interface DeveloperPlan {
|
||||
id: string
|
||||
name: string
|
||||
price: string
|
||||
value: number
|
||||
icon: React.ReactNode
|
||||
popular: boolean
|
||||
features: string[]
|
||||
description: string
|
||||
}
|
||||
|
||||
export default function HumanDeveloperPage() {
|
||||
const router = useRouter()
|
||||
const { user, balance, refreshBalance } = useAuth()
|
||||
const { toast } = useToast()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const [success, setSuccess] = useState('')
|
||||
|
||||
// Form state
|
||||
const [selectedPlan, setSelectedPlan] = useState('')
|
||||
const [requirements, setRequirements] = useState('')
|
||||
const [contactInfo, setContactInfo] = useState({
|
||||
name: user?.name || '',
|
||||
email: user?.email || '',
|
||||
phone: '',
|
||||
})
|
||||
|
||||
// Available developer plans
|
||||
const plans: DeveloperPlan[] = [
|
||||
{
|
||||
id: 'hourly',
|
||||
name: 'Hourly',
|
||||
price: '₹2,000',
|
||||
value: 2000,
|
||||
icon: <Clock className="w-5 h-5" />,
|
||||
popular: false,
|
||||
description: 'Perfect for quick fixes and consultations',
|
||||
features: [
|
||||
'Flexible hours',
|
||||
'Pay as you go',
|
||||
'Minimum 2 hours',
|
||||
'Quick fixes & bug fixes',
|
||||
'Technical consultation',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'daily',
|
||||
name: 'Daily',
|
||||
price: '₹5,000',
|
||||
value: 5000,
|
||||
icon: <Calendar className="w-5 h-5" />,
|
||||
popular: true,
|
||||
description: 'Best for short-term projects and rapid development',
|
||||
features: [
|
||||
'8 hours per day',
|
||||
'Priority support',
|
||||
'Daily progress reports',
|
||||
'Dedicated developer',
|
||||
'Progress tracking',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'monthly',
|
||||
name: 'Monthly',
|
||||
price: '₹1,00,000',
|
||||
value: 100000,
|
||||
icon: <CalendarDays className="w-5 h-5" />,
|
||||
popular: false,
|
||||
description: 'Complete solution for large projects',
|
||||
features: [
|
||||
'Full-time developer (160+ hours)',
|
||||
'Dedicated project manager',
|
||||
'Weekly sprint planning',
|
||||
'Complete project planning',
|
||||
'Priority feature development',
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
// Technology expertise areas
|
||||
const techStack = [
|
||||
'React & Next.js',
|
||||
'Node.js',
|
||||
'Python',
|
||||
'PHP',
|
||||
'Laravel',
|
||||
'Vue.js',
|
||||
'Angular',
|
||||
'React Native',
|
||||
'Flutter',
|
||||
'WordPress',
|
||||
'Shopify',
|
||||
'Database Design',
|
||||
'API Development',
|
||||
'DevOps',
|
||||
'UI/UX Design',
|
||||
'Mobile Apps',
|
||||
'E-commerce',
|
||||
'CMS Development',
|
||||
]
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setError('')
|
||||
setSuccess('')
|
||||
|
||||
if (!user) {
|
||||
router.push('/auth?redirect=/services/human-developer')
|
||||
return
|
||||
}
|
||||
|
||||
// Validate form
|
||||
if (!selectedPlan) {
|
||||
setError('Please select a plan')
|
||||
return
|
||||
}
|
||||
|
||||
if (!requirements.trim()) {
|
||||
setError('Please describe your project requirements')
|
||||
return
|
||||
}
|
||||
|
||||
if (!contactInfo.name || !contactInfo.email) {
|
||||
setError('Please fill in all contact information')
|
||||
return
|
||||
}
|
||||
|
||||
const selectedPlanData = plans.find((p) => p.id === selectedPlan)
|
||||
if (!selectedPlanData) {
|
||||
setError('Invalid plan selected')
|
||||
return
|
||||
}
|
||||
|
||||
// Check balance before proceeding
|
||||
const minimumDeposit = selectedPlanData.value * 0.5
|
||||
if (!validateBalance(balance || 0, minimumDeposit)) {
|
||||
setError(
|
||||
`Insufficient balance. Minimum deposit required: ${formatCurrency(minimumDeposit)}, Available: ${formatCurrency(balance || 0)}`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/services/hire-developer', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
planId: selectedPlan,
|
||||
planName: selectedPlanData.name,
|
||||
planPrice: selectedPlanData.value,
|
||||
requirements: requirements.trim(),
|
||||
contactInfo: {
|
||||
name: contactInfo.name.trim(),
|
||||
email: contactInfo.email.trim(),
|
||||
phone: contactInfo.phone?.trim() || undefined,
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error?.message || 'Failed to submit request')
|
||||
}
|
||||
|
||||
if (data.success) {
|
||||
setSuccess(data.data.message)
|
||||
toast({
|
||||
title: 'Request Submitted Successfully!',
|
||||
description: `Deposit of ${formatCurrency(minimumDeposit)} has been deducted. Request ID: ${data.data.requestId}`,
|
||||
})
|
||||
|
||||
// Refresh user balance
|
||||
await refreshBalance()
|
||||
|
||||
// Reset form
|
||||
setSelectedPlan('')
|
||||
setRequirements('')
|
||||
setContactInfo({
|
||||
name: user?.name || '',
|
||||
email: user?.email || '',
|
||||
phone: '',
|
||||
})
|
||||
} else {
|
||||
throw new Error(data.error?.message || 'Failed to submit request')
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Developer hire error:', err)
|
||||
const errorMessage = err instanceof Error ? err.message : 'Failed to submit request'
|
||||
setError(errorMessage)
|
||||
toast({
|
||||
title: 'Request Failed',
|
||||
description: errorMessage,
|
||||
variant: 'destructive',
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<Header />
|
||||
|
||||
<div className="container mx-auto px-4 pt-24 pb-16">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
{/* Page Header */}
|
||||
<div className="text-center mb-12">
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 bg-gradient-to-br from-blue-500 to-purple-600 rounded-full mb-4">
|
||||
<Users className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
<h1 className="text-4xl font-bold mb-4">Hire Expert Developers</h1>
|
||||
<p className="text-xl text-muted-foreground max-w-3xl mx-auto">
|
||||
Get skilled developers for your projects. From quick fixes to full-scale applications.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{!user && (
|
||||
<Alert className="mb-8">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
You need to be logged in to hire developers.{' '}
|
||||
<a
|
||||
href="/auth?redirect=/services/human-developer"
|
||||
className="text-primary underline"
|
||||
>
|
||||
Click here to login
|
||||
</a>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{success && (
|
||||
<Alert className="mb-8">
|
||||
<CheckCircle className="h-4 w-4" />
|
||||
<AlertDescription>{success}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="space-y-8">
|
||||
{/* Plan Selection */}
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold mb-6">Choose Your Plan</h2>
|
||||
<RadioGroup value={selectedPlan} onValueChange={setSelectedPlan}>
|
||||
<div className="grid md:grid-cols-3 gap-6">
|
||||
{plans.map((plan) => (
|
||||
<div key={plan.id} className="relative">
|
||||
<RadioGroupItem value={plan.id} id={plan.id} className="peer sr-only" />
|
||||
<Label
|
||||
htmlFor={plan.id}
|
||||
className={`block cursor-pointer transition-all rounded-xl hover:shadow-lg ${
|
||||
plan.popular ? 'ring-2 ring-primary' : ''
|
||||
}`}
|
||||
>
|
||||
<Card
|
||||
className={`h-full transition-all ${
|
||||
selectedPlan === plan.id
|
||||
? 'border-primary border-2 shadow-lg bg-primary/5 ring-2 ring-primary/20'
|
||||
: 'hover:border-primary/50'
|
||||
}`}
|
||||
>
|
||||
{plan.popular && (
|
||||
<div className="absolute -top-3 left-1/2 transform -translate-x-1/2">
|
||||
<Badge className="bg-gradient-to-r from-blue-500 to-purple-600">
|
||||
<Star className="w-3 h-3 mr-1" />
|
||||
Most Popular
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedPlan === plan.id && (
|
||||
<div className="absolute -top-3 left-4">
|
||||
<Badge className="bg-gradient-to-r from-blue-500 to-purple-600 text-white">
|
||||
<CheckCircle className="w-3 h-3 mr-1" />
|
||||
Selected
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<CardHeader className="text-center pb-4">
|
||||
<div className="relative">
|
||||
<div className="w-12 h-12 bg-gradient-to-br from-blue-500 to-purple-600 rounded-full flex items-center justify-center mx-auto mb-3 text-white">
|
||||
{plan.icon}
|
||||
</div>
|
||||
{/* {selectedPlan === plan.id && (
|
||||
<div className="absolute -top-1 -right-1 w-6 h-6 bg-gradient-to-br from-blue-500 to-purple-600 rounded-full flex items-center justify-center border-2 border-white">
|
||||
<CheckCircle className="w-4 h-4 text-white" />
|
||||
</div>
|
||||
)} */}
|
||||
</div>
|
||||
<CardTitle className="text-xl">{plan.name}</CardTitle>
|
||||
<div className="text-3xl font-bold text-primary">
|
||||
{plan.price}
|
||||
<span className="text-sm font-normal text-muted-foreground">
|
||||
/{plan.id}
|
||||
</span>
|
||||
</div>
|
||||
<CardDescription className="mt-2">{plan.description}</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
<ul className="space-y-2">
|
||||
{plan.features.map((feature, idx) => (
|
||||
<li key={idx} className="flex items-start gap-2">
|
||||
<CheckCircle className="w-4 h-4 text-green-500 mt-0.5 flex-shrink-0" />
|
||||
<span className="text-sm">{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
|
||||
{/* Project Requirements */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Project Requirements</CardTitle>
|
||||
<CardDescription>
|
||||
Describe your project in detail to help us match you with the right developer
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="requirements">Project Description *</Label>
|
||||
<Textarea
|
||||
id="requirements"
|
||||
rows={6}
|
||||
placeholder="Please describe your project requirements, technology stack, timeline, and any specific needs..."
|
||||
value={requirements}
|
||||
onChange={(e) => setRequirements(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Technology Stack */}
|
||||
<div>
|
||||
<Label>Our Technology Expertise</Label>
|
||||
<div className="flex flex-wrap gap-2 mt-2">
|
||||
{techStack.map((tech, idx) => (
|
||||
<Badge key={idx} variant="secondary" className="text-xs">
|
||||
{tech}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Contact Information */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Contact Information</CardTitle>
|
||||
<CardDescription>We'll use this to get in touch with you</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="name">Full Name *</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={contactInfo.name}
|
||||
onChange={(e) => setContactInfo({ ...contactInfo, name: e.target.value })}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="email">Email Address *</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
value={contactInfo.email}
|
||||
onChange={(e) => setContactInfo({ ...contactInfo, email: e.target.value })}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="phone">Phone Number</Label>
|
||||
<Input
|
||||
id="phone"
|
||||
type="tel"
|
||||
placeholder="+91 98765 43210"
|
||||
value={contactInfo.phone}
|
||||
onChange={(e) => setContactInfo({ ...contactInfo, phone: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Balance Check & Order Summary */}
|
||||
{selectedPlan && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Order Summary</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Plan Selected</span>
|
||||
<span>{plans.find((p) => p.id === selectedPlan)?.name}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Rate</span>
|
||||
<span>{plans.find((p) => p.id === selectedPlan)?.price}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Minimum Deposit (50%)</span>
|
||||
<span className="text-orange-600">
|
||||
{formatCurrency(
|
||||
(plans.find((p) => p.id === selectedPlan)?.value || 0) * 0.5
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Your Balance</span>
|
||||
<span
|
||||
className={
|
||||
balance &&
|
||||
balance >= (plans.find((p) => p.id === selectedPlan)?.value || 0) * 0.5
|
||||
? 'text-green-600'
|
||||
: 'text-red-600'
|
||||
}
|
||||
>
|
||||
{formatCurrency(balance || 0)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="border-t pt-2">
|
||||
<div className="flex justify-between font-semibold">
|
||||
<span>Deposit Required</span>
|
||||
<span className="text-primary">
|
||||
{formatCurrency(
|
||||
(plans.find((p) => p.id === selectedPlan)?.value || 0) * 0.5
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 p-3 bg-muted/50 rounded-lg">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
* A 50% deposit is required to start. Final pricing will be discussed based
|
||||
on your specific requirements and project scope.
|
||||
</p>
|
||||
</div>
|
||||
{balance &&
|
||||
balance < (plans.find((p) => p.id === selectedPlan)?.value || 0) * 0.5 && (
|
||||
<Alert className="mt-4">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
Insufficient balance. Please{' '}
|
||||
<a href="/balance" className="text-primary underline">
|
||||
add funds to your account
|
||||
</a>{' '}
|
||||
before proceeding.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Submit Button */}
|
||||
<Button type="submit" className="w-full" size="lg" disabled={loading || !user}>
|
||||
{loading ? 'Submitting Request...' : 'Submit Request & Proceed to Payment'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{/* Why Choose Us */}
|
||||
<div className="mt-16">
|
||||
<h3 className="text-2xl font-semibold mb-8 text-center">Why Choose Our Developers?</h3>
|
||||
<div className="grid md:grid-cols-4 gap-6">
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="text-center">
|
||||
<div className="inline-flex items-center justify-center w-12 h-12 bg-primary/10 rounded-full mb-3">
|
||||
<Code className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<h4 className="font-semibold mb-2">Expert Skills</h4>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
5+ years experience in modern tech stacks
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="text-center">
|
||||
<div className="inline-flex items-center justify-center w-12 h-12 bg-primary/10 rounded-full mb-3">
|
||||
<CheckCircle className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<h4 className="font-semibold mb-2">Quality Assured</h4>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Code reviews and testing included
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="text-center">
|
||||
<div className="inline-flex items-center justify-center w-12 h-12 bg-primary/10 rounded-full mb-3">
|
||||
<Clock className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<h4 className="font-semibold mb-2">On-Time Delivery</h4>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Meeting deadlines is our priority
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="text-center">
|
||||
<div className="inline-flex items-center justify-center w-12 h-12 bg-primary/10 rounded-full mb-3">
|
||||
<Briefcase className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<h4 className="font-semibold mb-2">Full Support</h4>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Post-delivery support and maintenance
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
642
app/services/kubernetes/page.tsx
Normal file
642
app/services/kubernetes/page.tsx
Normal file
@@ -0,0 +1,642 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { Header } from '@/components/header'
|
||||
import { Footer } from '@/components/footer'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import {
|
||||
Container,
|
||||
Server,
|
||||
HardDrive,
|
||||
Shield,
|
||||
AlertCircle,
|
||||
Check,
|
||||
Plus,
|
||||
Minus,
|
||||
Cpu,
|
||||
GitBranch,
|
||||
Download,
|
||||
ExternalLink,
|
||||
} from 'lucide-react'
|
||||
import { useAuth } from '@/contexts/AuthContext'
|
||||
|
||||
interface NodePool {
|
||||
id: string
|
||||
name: string
|
||||
size: string
|
||||
count: number
|
||||
autoscale: boolean
|
||||
minNodes?: number
|
||||
maxNodes?: number
|
||||
}
|
||||
|
||||
interface ClusterSize {
|
||||
id: string
|
||||
name: string
|
||||
price: {
|
||||
daily: number
|
||||
monthly: number
|
||||
}
|
||||
}
|
||||
|
||||
interface DeploymentResult {
|
||||
status: string
|
||||
clusterId?: string
|
||||
message?: string
|
||||
id?: string
|
||||
error?: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export default function KubernetesPage() {
|
||||
const router = useRouter()
|
||||
const { user } = useAuth()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const [success, setSuccess] = useState('')
|
||||
const [deploymentResult, setDeploymentResult] = useState<DeploymentResult | null>(null)
|
||||
const [billingCycle, setBillingCycle] = useState<'daily' | 'monthly'>('monthly')
|
||||
|
||||
// Form state
|
||||
const [clusterName, setClusterName] = useState('')
|
||||
const [k8sVersion, setK8sVersion] = useState('1.30.0-utho')
|
||||
const [nodePools, setNodePools] = useState<NodePool[]>([
|
||||
{
|
||||
id: '1',
|
||||
name: 'default-pool',
|
||||
size: '10215',
|
||||
count: 2,
|
||||
autoscale: false,
|
||||
minNodes: 2,
|
||||
maxNodes: 4,
|
||||
},
|
||||
])
|
||||
|
||||
// Available cluster sizes
|
||||
const clusterSizes: ClusterSize[] = [
|
||||
{ id: '10215', name: '2 vCPU, 4GB RAM', price: { daily: 200, monthly: 4000 } },
|
||||
{ id: '10216', name: '4 vCPU, 8GB RAM', price: { daily: 300, monthly: 7000 } },
|
||||
{ id: '10217', name: '8 vCPU, 16GB RAM', price: { daily: 500, monthly: 12000 } },
|
||||
{ id: '10218', name: '16 vCPU, 32GB RAM', price: { daily: 800, monthly: 20000 } },
|
||||
]
|
||||
|
||||
// Kubernetes versions
|
||||
const k8sVersions = ['1.30.0-utho', '1.29.0-utho', '1.28.0-utho']
|
||||
|
||||
// Mock user balance
|
||||
const userBalance = 15000
|
||||
|
||||
// Calculate total price
|
||||
const calculatePrice = () => {
|
||||
let total = 0
|
||||
nodePools.forEach((pool) => {
|
||||
const size = clusterSizes.find((s) => s.id === pool.size)
|
||||
if (size) {
|
||||
total += size.price[billingCycle] * pool.count
|
||||
}
|
||||
})
|
||||
return total
|
||||
}
|
||||
|
||||
// Add node pool
|
||||
const addNodePool = () => {
|
||||
const newPool: NodePool = {
|
||||
id: Date.now().toString(),
|
||||
name: `pool-${nodePools.length + 1}`,
|
||||
size: '10215',
|
||||
count: 1,
|
||||
autoscale: false,
|
||||
minNodes: 1,
|
||||
maxNodes: 3,
|
||||
}
|
||||
setNodePools([...nodePools, newPool])
|
||||
}
|
||||
|
||||
// Remove node pool
|
||||
const removeNodePool = (id: string) => {
|
||||
if (nodePools.length > 1) {
|
||||
setNodePools(nodePools.filter((pool) => pool.id !== id))
|
||||
}
|
||||
}
|
||||
|
||||
// Update node pool
|
||||
const updateNodePool = (id: string, field: keyof NodePool, value: any) => {
|
||||
setNodePools(nodePools.map((pool) => (pool.id === id ? { ...pool, [field]: value } : pool)))
|
||||
}
|
||||
|
||||
// Format node pools for API
|
||||
const formatNodePools = () => {
|
||||
return nodePools.map((pool) => ({
|
||||
name: pool.name,
|
||||
size: pool.size,
|
||||
count: pool.count,
|
||||
autoscale: pool.autoscale,
|
||||
min_nodes: pool.minNodes || pool.count,
|
||||
max_nodes: pool.maxNodes || pool.count + 2,
|
||||
}))
|
||||
}
|
||||
|
||||
const handleDeploy = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setError('')
|
||||
setSuccess('')
|
||||
setDeploymentResult(null)
|
||||
|
||||
if (!user) {
|
||||
router.push('/auth?redirect=/services/kubernetes')
|
||||
return
|
||||
}
|
||||
|
||||
// Validate form
|
||||
if (!clusterName) {
|
||||
setError('Cluster name is required')
|
||||
return
|
||||
}
|
||||
|
||||
// Validate cluster name
|
||||
if (!/^[a-z0-9-]+$/.test(clusterName)) {
|
||||
setError('Cluster name can only contain lowercase letters, numbers, and hyphens')
|
||||
return
|
||||
}
|
||||
|
||||
// Check balance
|
||||
const totalPrice = calculatePrice()
|
||||
if (userBalance < totalPrice) {
|
||||
setError(
|
||||
`Insufficient balance. You need ₹${totalPrice.toFixed(2)} but only have ₹${userBalance.toFixed(2)}`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/services/deploy-kubernetes', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
cluster_label: clusterName,
|
||||
cluster_version: k8sVersion,
|
||||
nodepools: formatNodePools(),
|
||||
cycle: billingCycle,
|
||||
amount: totalPrice,
|
||||
}),
|
||||
})
|
||||
|
||||
const result: DeploymentResult = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(result.message || `Deployment failed with status ${response.status}`)
|
||||
}
|
||||
|
||||
if (result.status === 'success') {
|
||||
setDeploymentResult(result)
|
||||
setSuccess(
|
||||
`Kubernetes cluster "${clusterName}" deployed successfully! Cluster ID: ${result.clusterId || result.id}`
|
||||
)
|
||||
|
||||
// Reset form after successful deployment
|
||||
setClusterName('')
|
||||
setNodePools([
|
||||
{
|
||||
id: '1',
|
||||
name: 'default-pool',
|
||||
size: '10215',
|
||||
count: 2,
|
||||
autoscale: false,
|
||||
minNodes: 2,
|
||||
maxNodes: 4,
|
||||
},
|
||||
])
|
||||
} else {
|
||||
throw new Error(result.message || 'Deployment failed')
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Deployment error:', err)
|
||||
setError(err instanceof Error ? err.message : 'Failed to deploy Kubernetes cluster')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const downloadKubeconfig = async (clusterId: string) => {
|
||||
try {
|
||||
const response = await fetch(`/api/services/deploy-kubernetes?clusterId=${clusterId}`)
|
||||
|
||||
if (response.ok) {
|
||||
const blob = await response.blob()
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.style.display = 'none'
|
||||
a.href = url
|
||||
a.download = `kubeconfig-${clusterId}.yaml`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
window.URL.revokeObjectURL(url)
|
||||
document.body.removeChild(a)
|
||||
} else {
|
||||
const errorData = await response.json()
|
||||
setError(errorData.message || 'Failed to download kubeconfig')
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Kubeconfig download error:', err)
|
||||
setError('Failed to download kubeconfig')
|
||||
}
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
setDeploymentResult(null)
|
||||
setSuccess('')
|
||||
setError('')
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<Header />
|
||||
|
||||
<div className="container mx-auto px-4 pt-24 pb-16">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
{/* Page Header */}
|
||||
<div className="text-center mb-12">
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 bg-gradient-to-br from-blue-500 to-purple-600 rounded-full mb-4">
|
||||
<Container className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
<h1 className="text-4xl font-bold mb-4">Kubernetes Edge</h1>
|
||||
<p className="text-xl text-muted-foreground">
|
||||
Managed Kubernetes clusters with auto-scaling and enterprise-grade security
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{!user && (
|
||||
<Alert className="mb-8">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
You need to be logged in to deploy a Kubernetes cluster.{' '}
|
||||
<a href="/auth?redirect=/services/kubernetes" className="text-primary underline">
|
||||
Click here to login
|
||||
</a>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{deploymentResult ? (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Deployment Successful!</CardTitle>
|
||||
<CardDescription>
|
||||
Your Kubernetes cluster has been deployed successfully.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label>Cluster ID</Label>
|
||||
<p className="text-sm font-mono bg-muted p-2 rounded">
|
||||
{deploymentResult.clusterId || deploymentResult.id}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Status</Label>
|
||||
<p className="text-sm capitalize">{deploymentResult.status}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4">
|
||||
{deploymentResult.clusterId && (
|
||||
<Button onClick={() => downloadKubeconfig(deploymentResult.clusterId!)}>
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
Download Kubeconfig
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="outline" onClick={() => router.push('/dashboard/clusters')}>
|
||||
View Clusters
|
||||
</Button>
|
||||
<Button variant="outline" onClick={resetForm}>
|
||||
Deploy Another
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<form onSubmit={handleDeploy}>
|
||||
<div className="space-y-8">
|
||||
{/* Billing Cycle */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Billing Cycle</CardTitle>
|
||||
<CardDescription>Choose your preferred billing period</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Tabs
|
||||
value={billingCycle}
|
||||
onValueChange={(v) => setBillingCycle(v as 'daily' | 'monthly')}
|
||||
>
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="daily">Daily</TabsTrigger>
|
||||
<TabsTrigger value="monthly">Monthly (Save 25%)</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Cluster Configuration */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Cluster Configuration</CardTitle>
|
||||
<CardDescription>Configure your Kubernetes cluster settings</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="clusterName">Cluster Name</Label>
|
||||
<Input
|
||||
id="clusterName"
|
||||
placeholder="my-k8s-cluster"
|
||||
value={clusterName}
|
||||
onChange={(e) => setClusterName(e.target.value.toLowerCase())}
|
||||
pattern="^[a-z0-9-]+$"
|
||||
required
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Only lowercase letters, numbers, and hyphens allowed
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="k8sVersion">Kubernetes Version</Label>
|
||||
<Select value={k8sVersion} onValueChange={setK8sVersion}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{k8sVersions.map((version) => (
|
||||
<SelectItem key={version} value={version}>
|
||||
<div className="flex items-center gap-2">
|
||||
<GitBranch className="w-4 h-4" />v{version.replace('-utho', '')}
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="p-3 bg-blue-50 rounded-md border border-blue-200">
|
||||
<p className="text-sm text-blue-700">
|
||||
<strong>Location:</strong> Mumbai, India (Fixed)
|
||||
</p>
|
||||
<p className="text-xs text-blue-600 mt-1">
|
||||
All Kubernetes clusters are deployed in our Mumbai datacenter for optimal
|
||||
performance.
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Node Pools */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
Node Pools
|
||||
<Button type="button" size="sm" onClick={addNodePool} variant="outline">
|
||||
<Plus className="w-4 h-4 mr-1" />
|
||||
Add Pool
|
||||
</Button>
|
||||
</CardTitle>
|
||||
<CardDescription>Configure worker node pools for your cluster</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{nodePools.map((pool, index) => (
|
||||
<Card key={pool.id}>
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Input
|
||||
value={pool.name}
|
||||
onChange={(e) => updateNodePool(pool.id, 'name', e.target.value)}
|
||||
className="max-w-xs"
|
||||
placeholder="Pool name"
|
||||
required
|
||||
/>
|
||||
{nodePools.length > 1 && (
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => removeNodePool(pool.id)}
|
||||
>
|
||||
<Minus className="w-4 h-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label>Node Size</Label>
|
||||
<Select
|
||||
value={pool.size}
|
||||
onValueChange={(value) => updateNodePool(pool.id, 'size', value)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{clusterSizes.map((size) => (
|
||||
<SelectItem key={size.id} value={size.id}>
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<span>{size.name}</span>
|
||||
<span className="ml-4 text-primary text-sm">
|
||||
₹{size.price[billingCycle]}/
|
||||
{billingCycle === 'daily' ? 'day' : 'mo'}
|
||||
</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Node Count</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() =>
|
||||
updateNodePool(pool.id, 'count', Math.max(1, pool.count - 1))
|
||||
}
|
||||
>
|
||||
<Minus className="w-4 h-4" />
|
||||
</Button>
|
||||
<Input
|
||||
type="number"
|
||||
value={pool.count}
|
||||
onChange={(e) =>
|
||||
updateNodePool(pool.id, 'count', parseInt(e.target.value) || 1)
|
||||
}
|
||||
className="w-20 text-center"
|
||||
min="1"
|
||||
max="10"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() =>
|
||||
updateNodePool(pool.id, 'count', Math.min(10, pool.count + 1))
|
||||
}
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Pool cost: ₹
|
||||
{(clusterSizes.find((s) => s.id === pool.size)?.price[billingCycle] ||
|
||||
0) * pool.count}
|
||||
/{billingCycle === 'daily' ? 'day' : 'month'}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Pricing Summary */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Order Summary</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
{nodePools.map((pool) => {
|
||||
const size = clusterSizes.find((s) => s.id === pool.size)
|
||||
const cost = (size?.price[billingCycle] || 0) * pool.count
|
||||
return (
|
||||
<div key={pool.id} className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground">
|
||||
{pool.name} ({pool.count} nodes × {size?.name})
|
||||
</span>
|
||||
<span>₹{cost}</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<div className="border-t pt-2">
|
||||
<div className="flex justify-between font-semibold">
|
||||
<span>Total</span>
|
||||
<span className="text-primary">
|
||||
₹{calculatePrice()}/{billingCycle === 'daily' ? 'day' : 'month'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pt-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground">Your Balance</span>
|
||||
<span
|
||||
className={
|
||||
userBalance >= calculatePrice() ? 'text-green-600' : 'text-red-600'
|
||||
}
|
||||
>
|
||||
₹{userBalance.toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{error && (
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Deploy Button */}
|
||||
<Button type="submit" className="w-full" size="lg" disabled={loading || !user}>
|
||||
{loading ? (
|
||||
<>
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
||||
Deploying Cluster...
|
||||
</>
|
||||
) : (
|
||||
'Deploy Kubernetes Cluster'
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
|
||||
{/* Features */}
|
||||
{!deploymentResult && (
|
||||
<div className="mt-16 grid md:grid-cols-4 gap-6">
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="text-center">
|
||||
<Cpu className="w-8 h-8 text-primary mx-auto mb-3" />
|
||||
<h4 className="font-semibold mb-2">Auto-scaling</h4>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Automatically scale nodes based on workload
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="text-center">
|
||||
<Shield className="w-8 h-8 text-primary mx-auto mb-3" />
|
||||
<h4 className="font-semibold mb-2">Secure by Default</h4>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
RBAC, network policies, and encryption
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="text-center">
|
||||
<Server className="w-8 h-8 text-primary mx-auto mb-3" />
|
||||
<h4 className="font-semibold mb-2">Load Balancing</h4>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Built-in load balancers for your services
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="text-center">
|
||||
<HardDrive className="w-8 h-8 text-primary mx-auto mb-3" />
|
||||
<h4 className="font-semibold mb-2">Persistent Storage</h4>
|
||||
<p className="text-sm text-muted-foreground">SSD-backed persistent volumes</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
202
app/services/page.tsx
Normal file
202
app/services/page.tsx
Normal file
@@ -0,0 +1,202 @@
|
||||
import { Header } from '@/components/header'
|
||||
import { Footer } from '@/components/footer'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { CustomSolutionCTA } from '@/components/ui/custom-solution-cta'
|
||||
import {
|
||||
Server,
|
||||
Database,
|
||||
Mail,
|
||||
Shield,
|
||||
Container,
|
||||
Cog,
|
||||
ArrowRight,
|
||||
Check,
|
||||
Users,
|
||||
Cloud,
|
||||
} from 'lucide-react'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
|
||||
export default function ServicesPage() {
|
||||
// Simulating database fetch - in production this would be from API/database
|
||||
const services = [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Cloud Instance',
|
||||
description: 'High-performance cloud servers with instant deployment and scalable resources',
|
||||
price: 'Starting at ₹200/day',
|
||||
imageUrl: '/assets/images/services/cloud-instance.jpg',
|
||||
features: ['Instant Deployment', 'Auto Scaling', 'Load Balancing', '99.99% Uptime SLA'],
|
||||
learnMoreUrl: '/services/cloud-instance',
|
||||
buyButtonUrl: '/services/cloud-instance',
|
||||
buyButtonText: 'Deploy Now',
|
||||
popular: false,
|
||||
icon: Cloud,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Kubernetes',
|
||||
description: 'Managed Kubernetes clusters with auto-scaling and enterprise-grade security',
|
||||
price: 'Starting at ₹4000/month',
|
||||
imageUrl: '/assets/images/services/kubernetes-edge.png',
|
||||
features: ['Auto-scaling', 'Load Balancing', 'CI/CD Ready', 'Built-in Monitoring'],
|
||||
learnMoreUrl: '/services/kubernetes',
|
||||
buyButtonUrl: '/services/kubernetes',
|
||||
buyButtonText: 'Create Cluster',
|
||||
popular: true,
|
||||
icon: Container,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: 'Hosting Control Panel',
|
||||
description: 'Professional web hosting with cPanel, Plesk, or DirectAdmin',
|
||||
price: 'Starting at ₹500/month',
|
||||
imageUrl: '/assets/images/services/hosting-cp.jpg',
|
||||
features: ['cPanel/Plesk/DirectAdmin', 'One-Click Apps', 'SSL Certificates', 'Daily Backups'],
|
||||
learnMoreUrl: '/services/hosting-control-panel',
|
||||
buyButtonUrl: '/services/hosting-control-panel',
|
||||
buyButtonText: 'Get Started',
|
||||
popular: false,
|
||||
icon: Cog,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: 'VPN Services',
|
||||
description: 'Secure WireGuard VPN with QR code setup and multiple locations',
|
||||
price: 'Starting at ₹300/month',
|
||||
imageUrl: '/assets/images/services/wiregurd-thumb.jpg',
|
||||
features: ['WireGuard Protocol', 'QR Code Setup', 'Multiple Locations', 'No Logs Policy'],
|
||||
learnMoreUrl: '/services/vpn',
|
||||
buyButtonUrl: '/services/vpn',
|
||||
buyButtonText: 'Setup VPN',
|
||||
popular: false,
|
||||
icon: Shield,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: 'Human Developer',
|
||||
description: 'Skilled developers for your custom projects and requirements',
|
||||
price: 'Custom Quote',
|
||||
imageUrl: '/assets/images/services/human-developer.jpg',
|
||||
features: ['Expert Developers', 'Custom Solutions', 'Project Management', '24/7 Support'],
|
||||
learnMoreUrl: '/services/human-developer',
|
||||
buyButtonUrl: '/services/human-developer',
|
||||
buyButtonText: 'Hire Developer',
|
||||
popular: false,
|
||||
icon: Users,
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
title: 'VPS Hosting',
|
||||
description: 'Virtual private servers with full root access and dedicated resources',
|
||||
price: 'Starting at ₹800/month',
|
||||
imageUrl: '/assets/images/services/vps_thumb.jpg',
|
||||
features: ['Full Root Access', 'SSD Storage', 'Multiple OS Options', 'IPv6 Support'],
|
||||
learnMoreUrl: '/services/vps',
|
||||
buyButtonUrl: '/services/vps',
|
||||
buyButtonText: 'Order VPS',
|
||||
popular: false,
|
||||
icon: Server,
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<Header />
|
||||
<div className="container mx-auto px-4 pt-24 pb-16">
|
||||
{/* Page Header */}
|
||||
<div className="text-center mb-12">
|
||||
<h1 className="text-4xl font-bold mb-4">Our Premium Services</h1>
|
||||
<p className="text-xl text-muted-foreground max-w-3xl mx-auto">
|
||||
Tailored solutions to accelerate your business growth
|
||||
</p>
|
||||
<div className="w-24 h-1 bg-gradient-to-r from-blue-500 to-purple-600 mx-auto mt-6"></div>
|
||||
</div>
|
||||
|
||||
{/* Services Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{services.map((service) => {
|
||||
const Icon = service.icon
|
||||
return (
|
||||
<Card
|
||||
key={service.id}
|
||||
className={`relative overflow-hidden hover:shadow-lg transition-shadow ${service.popular ? 'ring-2 ring-primary' : ''}`}
|
||||
>
|
||||
{/* Most Popular Badge */}
|
||||
{service.popular && (
|
||||
<div className="absolute top-4 right-4 z-10">
|
||||
<span className="bg-gradient-to-r from-blue-500 to-purple-600 text-white px-3 py-1 rounded-full text-xs font-semibold">
|
||||
Most Popular
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Service Thumbnail */}
|
||||
<div className="h-48 bg-gradient-to-br from-gray-100 to-gray-200 dark:from-gray-800 dark:to-gray-900 flex items-center justify-center">
|
||||
{service.imageUrl ? (
|
||||
<div className="relative w-full h-full">
|
||||
{/* Using div with background for now since images aren't available */}
|
||||
<div className="w-full h-full flex items-center justify-center bg-gradient-to-br from-blue-50 to-purple-50 dark:from-gray-800 dark:to-gray-900">
|
||||
<Icon className="w-16 h-16 text-primary opacity-20" />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Icon className="w-16 h-16 text-muted-foreground" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Service Content */}
|
||||
<CardHeader className="text-center">
|
||||
<CardTitle className="text-xl mb-2">{service.title}</CardTitle>
|
||||
<div className="text-lg font-semibold text-primary mb-2">{service.price}</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-4">
|
||||
<p className="text-sm text-muted-foreground text-center">{service.description}</p>
|
||||
|
||||
{/* Features List */}
|
||||
<ul className="space-y-2">
|
||||
{service.features.map((feature, idx) => (
|
||||
<li key={idx} className="flex items-start gap-2">
|
||||
<Check className="w-4 h-4 text-green-500 mt-0.5 flex-shrink-0" />
|
||||
<span className="text-sm text-muted-foreground">{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex gap-3 pt-4">
|
||||
{service.learnMoreUrl && (
|
||||
<Link href={service.learnMoreUrl} className="flex-1">
|
||||
<Button variant="outline" className="w-full">
|
||||
Learn More
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
{service.buyButtonUrl && service.buyButtonText && (
|
||||
<Link href={service.buyButtonUrl} className="flex-1">
|
||||
<Button className="w-full">
|
||||
{service.buyButtonText}
|
||||
<ArrowRight className="w-4 h-4 ml-1" />
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Additional Services Note */}
|
||||
<CustomSolutionCTA
|
||||
description="We offer tailored solutions for enterprise clients with specific requirements"
|
||||
buttonText="Contact Sales"
|
||||
buttonHref="/contact"
|
||||
/>
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
431
app/services/vpn/page.tsx
Normal file
431
app/services/vpn/page.tsx
Normal file
@@ -0,0 +1,431 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { Header } from '@/components/header'
|
||||
import { Footer } from '@/components/footer'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { Shield, MapPin, Download, QrCode, AlertCircle, Lock, Globe, Zap, Eye } from 'lucide-react'
|
||||
import { useAuth } from '@/contexts/AuthContext'
|
||||
import { env } from 'process'
|
||||
|
||||
interface VPNLocation {
|
||||
code: string
|
||||
name: string
|
||||
flag: string
|
||||
popular?: boolean
|
||||
endpoint: string
|
||||
}
|
||||
|
||||
export default function VPNPage() {
|
||||
const router = useRouter()
|
||||
const { user } = useAuth()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const [billingCycle, setBillingCycle] = useState<'monthly' | 'yearly'>('monthly')
|
||||
const [selectedLocation, setSelectedLocation] = useState('')
|
||||
const [showConfig, setShowConfig] = useState(false)
|
||||
const [apiEndpoint, setApiEndpoint] = useState('')
|
||||
const [mockConfig, setMockConfig] = useState()
|
||||
// VPN pricing
|
||||
const pricing = {
|
||||
monthly: 300,
|
||||
yearly: 4023, // Save 17%
|
||||
}
|
||||
|
||||
// Available VPN locations
|
||||
const locations: VPNLocation[] = [
|
||||
// { code: 'india', name: 'Mumbai, India', flag: '🇮🇳', popular: true },
|
||||
{
|
||||
code: 'america',
|
||||
name: 'America, USA',
|
||||
flag: '🇺🇸',
|
||||
popular: true,
|
||||
endpoint: 'https://wireguard-vpn.3027622.siliconpin.com/vpn',
|
||||
},
|
||||
{
|
||||
code: 'europe',
|
||||
name: 'Europe, UK',
|
||||
flag: '🇬🇧',
|
||||
popular: true,
|
||||
endpoint: 'https://wireguard.vps20.siliconpin.com/vpn',
|
||||
},
|
||||
// { code: 'singapore', name: 'Singapore', flag: '🇸🇬' },
|
||||
// { code: 'japan', name: 'Tokyo, Japan', flag: '🇯🇵' },
|
||||
// { code: 'canada', name: 'Toronto, Canada', flag: '🇨🇦' },
|
||||
]
|
||||
|
||||
// Mock user balance
|
||||
const userBalance = 8000
|
||||
|
||||
const handleSetupVPN = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setError('')
|
||||
|
||||
if (!user) {
|
||||
router.push('/auth?redirect=/services/vpn')
|
||||
return
|
||||
}
|
||||
|
||||
if (!selectedLocation) {
|
||||
setError('Please select a VPN location')
|
||||
return
|
||||
}
|
||||
|
||||
const totalPrice = pricing[billingCycle]
|
||||
if (userBalance < totalPrice) {
|
||||
setError(`Insufficient balance. You need ₹${totalPrice} but only have ₹${userBalance}`)
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
|
||||
try {
|
||||
// Generate a unique order ID (you might want to use a proper ID generation)
|
||||
const orderId = `vpn-${Date.now()}`
|
||||
// const orderId = `vpn-${Date.now()}-${user.id.slice(0, 8)}`
|
||||
|
||||
// Make API request to our backend
|
||||
const response = await fetch('/api/services/deploy-vpn', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
orderId,
|
||||
location: selectedLocation,
|
||||
plan: billingCycle,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
throw new Error(errorData.error || 'Failed to setup VPN service')
|
||||
}
|
||||
|
||||
const { data } = await response.json()
|
||||
|
||||
// Set the config content and show the config section
|
||||
setMockConfig(data.config)
|
||||
setShowConfig(true)
|
||||
} catch (err) {
|
||||
setError(err.message || 'Failed to setup VPN service')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const downloadConfig = () => {
|
||||
const blob = new Blob([mockConfig], { type: 'application/octet-stream' }) // force binary
|
||||
const url = URL.createObjectURL(blob)
|
||||
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
// quote filename properly
|
||||
a.setAttribute('download', `siliconpin-vpn-${selectedLocation}.conf`)
|
||||
a.setAttribute('type', 'application/octet-stream')
|
||||
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
console.log('selectedLocation', selectedLocation)
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<Header />
|
||||
|
||||
<div className="container mx-auto px-4 pt-24 pb-16">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
{/* Page Header */}
|
||||
<div className="text-center mb-12">
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 bg-gradient-to-br from-blue-500 to-purple-600 rounded-full mb-4">
|
||||
<Shield className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
<h1 className="text-4xl font-bold mb-4">WireGuard VPN</h1>
|
||||
<p className="text-xl text-muted-foreground">
|
||||
Secure, fast, and private VPN service with WireGuard protocol
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{!user && (
|
||||
<Alert className="mb-8">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
You need to be logged in to setup VPN.{' '}
|
||||
<a href="/auth?redirect=/services/vpn" className="text-primary underline">
|
||||
Click here to login
|
||||
</a>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{!showConfig ? (
|
||||
<form onSubmit={handleSetupVPN}>
|
||||
<div className="space-y-8">
|
||||
{/* Billing Cycle */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Billing Plan</CardTitle>
|
||||
<CardDescription>Choose your preferred billing cycle</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Tabs
|
||||
value={billingCycle}
|
||||
onValueChange={(v) => setBillingCycle(v as 'monthly' | 'yearly')}
|
||||
>
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="monthly">Monthly - ₹{pricing.monthly}</TabsTrigger>
|
||||
<TabsTrigger value="yearly">
|
||||
Yearly - ₹{pricing.yearly} (Save 17%)
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Location Selection */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Select VPN Location</CardTitle>
|
||||
<CardDescription>
|
||||
Choose the server location for your VPN connection
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<RadioGroup value={selectedLocation} onValueChange={setSelectedLocation}>
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
{locations.map((location) => (
|
||||
<div key={location.code} className="relative">
|
||||
<RadioGroupItem
|
||||
value={location.code}
|
||||
id={location.code}
|
||||
className="peer sr-only"
|
||||
/>
|
||||
<Label
|
||||
htmlFor={location.code}
|
||||
className={`flex items-center justify-between p-4 border-2 rounded-lg cursor-pointer transition-all hover:border-primary/50 peer-checked:border-primary peer-checked:bg-primary/5 ${
|
||||
location.popular ? 'border-primary/30' : 'border-border'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-2xl">{location.flag}</span>
|
||||
<div>
|
||||
<div className="font-medium">{location.name}</div>
|
||||
{location.popular && (
|
||||
<div className="text-xs text-primary font-medium">
|
||||
Most Popular
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<MapPin className="w-4 h-4 text-muted-foreground" />
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Order Summary */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Order Summary</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">WireGuard VPN</span>
|
||||
<span>₹{pricing[billingCycle]}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Billing Cycle</span>
|
||||
<span className="capitalize">{billingCycle}</span>
|
||||
</div>
|
||||
<div className="border-t pt-2">
|
||||
<div className="flex justify-between font-semibold">
|
||||
<span>Total</span>
|
||||
<span className="text-primary">₹{pricing[billingCycle]}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pt-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground">Your Balance</span>
|
||||
<span
|
||||
className={
|
||||
userBalance >= pricing[billingCycle]
|
||||
? 'text-green-600'
|
||||
: 'text-red-600'
|
||||
}
|
||||
>
|
||||
₹{userBalance.toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{error && (
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Setup Button */}
|
||||
<Button type="submit" className="w-full" size="lg" disabled={loading || !user}>
|
||||
{loading ? 'Setting up VPN...' : 'Setup VPN Service'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
/* VPN Configuration */
|
||||
<div className="space-y-8">
|
||||
<Alert>
|
||||
<Shield className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
Your VPN service has been activated! Download the configuration file below.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>VPN Configuration</CardTitle>
|
||||
<CardDescription>
|
||||
Use this configuration with any WireGuard client
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
{/* Config File */}
|
||||
<div>
|
||||
<Label>Configuration File</Label>
|
||||
<div className="mt-2 p-4 bg-muted rounded-lg">
|
||||
<pre className="text-xs font-mono whitespace-pre-wrap text-muted-foreground">
|
||||
{mockConfig}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Download Buttons */}
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
<Button onClick={downloadConfig} className="flex-1">
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
Download Config File
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex-1"
|
||||
onClick={() => alert('QR Code functionality would be implemented here')}
|
||||
>
|
||||
<QrCode className="w-4 h-4 mr-2" />
|
||||
Show QR Code
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Reset */}
|
||||
<div className="pt-4 border-t">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setShowConfig(false)}
|
||||
className="w-full"
|
||||
>
|
||||
Setup New VPN
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Setup Instructions */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Setup Instructions</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-3">
|
||||
<h4 className="font-medium">For Desktop/Laptop:</h4>
|
||||
<ol className="text-sm space-y-1 list-decimal list-inside text-muted-foreground">
|
||||
<li>Download and install WireGuard client from wireguard.com</li>
|
||||
<li>Import the downloaded configuration file</li>
|
||||
<li>Click "Activate" to connect to the VPN</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<h4 className="font-medium">For Mobile:</h4>
|
||||
<ol className="text-sm space-y-1 list-decimal list-inside text-muted-foreground">
|
||||
<li>Install WireGuard app from App Store/Play Store</li>
|
||||
<li>Scan the QR code or import the config file</li>
|
||||
<li>Toggle the connection on</li>
|
||||
</ol>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Features */}
|
||||
{!showConfig && (
|
||||
<div className="mt-16 grid md:grid-cols-4 gap-6">
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="text-center">
|
||||
<Zap className="w-8 h-8 text-primary mx-auto mb-3" />
|
||||
<h4 className="font-semibold mb-2">Lightning Fast</h4>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
WireGuard protocol for maximum speed
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="text-center">
|
||||
<Eye className="w-8 h-8 text-primary mx-auto mb-3" />
|
||||
<h4 className="font-semibold mb-2">No Logs Policy</h4>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
We don't track or store your activity
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="text-center">
|
||||
<Lock className="w-8 h-8 text-primary mx-auto mb-3" />
|
||||
<h4 className="font-semibold mb-2">Strong Encryption</h4>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
ChaCha20 encryption for security
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="text-center">
|
||||
<Globe className="w-8 h-8 text-primary mx-auto mb-3" />
|
||||
<h4 className="font-semibold mb-2">Multiple Locations</h4>
|
||||
<p className="text-sm text-muted-foreground">Servers across the globe</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
162
app/sitemap.ts
Normal file
162
app/sitemap.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
import { MetadataRoute } from 'next'
|
||||
import TopicModel from '@/models/topic'
|
||||
|
||||
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:4023'
|
||||
|
||||
// Static pages
|
||||
const staticPages = [
|
||||
{
|
||||
url: baseUrl,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'daily' as const,
|
||||
priority: 1,
|
||||
},
|
||||
{
|
||||
url: `${baseUrl}/about`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'monthly' as const,
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: `${baseUrl}/services`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'weekly' as const,
|
||||
priority: 0.9,
|
||||
},
|
||||
{
|
||||
url: `${baseUrl}/services/cloud-instance`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'weekly' as const,
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: `${baseUrl}/services/hosting-control-panel`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'weekly' as const,
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: `${baseUrl}/services/kubernetes`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'weekly' as const,
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: `${baseUrl}/services/vpn`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'weekly' as const,
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: `${baseUrl}/services/human-developer`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'weekly' as const,
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: `${baseUrl}/tools`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'weekly' as const,
|
||||
priority: 0.7,
|
||||
},
|
||||
{
|
||||
url: `${baseUrl}/tools/speech-to-text`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'monthly' as const,
|
||||
priority: 0.6,
|
||||
},
|
||||
{
|
||||
url: `${baseUrl}/contact`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'monthly' as const,
|
||||
priority: 0.7,
|
||||
},
|
||||
{
|
||||
url: `${baseUrl}/topics`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'daily' as const,
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: `${baseUrl}/search`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'daily' as const,
|
||||
priority: 0.7,
|
||||
},
|
||||
{
|
||||
url: `${baseUrl}/feedback`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'monthly' as const,
|
||||
priority: 0.6,
|
||||
},
|
||||
{
|
||||
url: `${baseUrl}/auth`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'monthly' as const,
|
||||
priority: 0.5,
|
||||
},
|
||||
{
|
||||
url: `${baseUrl}/terms`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'yearly' as const,
|
||||
priority: 0.3,
|
||||
},
|
||||
{
|
||||
url: `${baseUrl}/privacy`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'yearly' as const,
|
||||
priority: 0.3,
|
||||
},
|
||||
{
|
||||
url: `${baseUrl}/legal-agreement`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'yearly' as const,
|
||||
priority: 0.3,
|
||||
},
|
||||
{
|
||||
url: `${baseUrl}/refund-policy`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'yearly' as const,
|
||||
priority: 0.3,
|
||||
},
|
||||
]
|
||||
|
||||
// Dynamic topic pages
|
||||
let topicPages: MetadataRoute.Sitemap = []
|
||||
let tagPages: MetadataRoute.Sitemap = []
|
||||
|
||||
try {
|
||||
// Get published topic posts
|
||||
const topics = await TopicModel.find({ isDraft: false })
|
||||
.select('slug publishedAt updatedAt')
|
||||
.lean()
|
||||
|
||||
topicPages = topics.map((topic) => ({
|
||||
url: `${baseUrl}/topics/${topic.slug}`,
|
||||
lastModified: new Date(topic.publishedAt),
|
||||
changeFrequency: 'weekly' as const,
|
||||
priority: 0.6,
|
||||
}))
|
||||
|
||||
// Get unique tags from all topics
|
||||
const allTopics = await TopicModel.find({ isDraft: false }).select('tags').lean()
|
||||
|
||||
const tagsSet = new Set<string>()
|
||||
allTopics.forEach((topic) => {
|
||||
topic.tags?.forEach((tag) => tagsSet.add(tag.name))
|
||||
})
|
||||
|
||||
tagPages = Array.from(tagsSet).map((tagName) => ({
|
||||
url: `${baseUrl}/topics?tag=${encodeURIComponent(tagName)}`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'weekly' as const,
|
||||
priority: 0.5,
|
||||
}))
|
||||
} catch (error) {
|
||||
console.error('Error generating dynamic sitemap entries:', error)
|
||||
// Continue with static pages only if database is unavailable
|
||||
}
|
||||
|
||||
return [...staticPages, ...topicPages, ...tagPages]
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user