initial commit

main
Kar k1 2025-08-30 18:18:57 +05:30
commit 7219108342
270 changed files with 70221 additions and 0 deletions

View 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
View File

@ -0,0 +1,8 @@
node_modules
.next
Dockerfile
docker-compose*
.git
.gitignore
.env*
README.md

10
.editorconfig Normal file
View 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
View 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
View 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
View 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
View File

@ -0,0 +1 @@
yarn lint-staged

462
CLAUDE.md Normal file
View 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
View 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
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 KiB

190
app/about/page.tsx Normal file
View 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>
)
}

View 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>
)
}

View 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
View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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
View 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>
)
}

View 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 })
}
})
}

View 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 })
}
})
}

View 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')
}

View 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 })
}
})
}

View 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 })
}
})
}

View 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 })
}
})
}

View 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 }
)
}
}

View 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))
}
}

View 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 }
)
}
}

View 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 }
)
}
}

View 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
View 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 }
)
}
})

View 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 }
)
}
}

View 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 }
)
}
}

View 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',
},
]
}

View 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 }
)
}
}

View 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 }
}

View 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
View 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 }
)
}
}

View 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
View 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
View 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 }
)
}
}

View 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
View 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
View 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 })
}
}

View 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
}

View 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',
}
}

View 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 }
}

View 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 }
)
}
}

View 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 }
)
}
}

View 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 }
)
}
}

View 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 })
}
}

View 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 })
}
}

View 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 }
)
}
}

View 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
View 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 }
)
}
}

View 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 })
}
}

View 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
View 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
View 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 }
)
}
}

View 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 }
)
}
}

View 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 }
)
}
}

View 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 }
)
}
}

View 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
View 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 }
)
}
}

View 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 }
)
}
}

View 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 }
)
}
}

View 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
View 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 })
}
}

View 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
View 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
View 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
View 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>
)
}

View 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
View 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
View 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
View 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
View 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>
)
}

View 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
View 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 />
}

View 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
View 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
View 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
View 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>
)
}

View 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
View 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

File diff suppressed because it is too large Load Diff

303
app/payment/failed/page.tsx Normal file
View 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>
)
}

View 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
View 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
View 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
View 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
View 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,
}
}

View 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"
// }

View 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>
)
}

View 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>
)
}

View 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
View 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
View 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
View 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