From 486f45bb9220fddbc94677eaae6e0b3b20a75acb Mon Sep 17 00:00:00 2001 From: Aditya Siregar Date: Fri, 15 Aug 2025 23:03:15 +0700 Subject: [PATCH] Add dockre file --- .dockerignore | 63 + .gitignore | 27 + .idea/.gitignore | 5 + .idea/e-voting-platform.iml | 12 + .idea/modules.xml | 8 + .idea/vcs.xml | 6 + AUTH_HEADER_SETUP.md | 102 + BACKEND_CONFIG.md | 68 + Dockerfile | 28 + ENV_SETUP.md | 49 + README.md | 98 + .../events/[eventId]/candidates/page.tsx | 513 ++ app/admin/events/page.tsx | 490 ++ app/admin/members/loading.tsx | 3 + app/admin/members/page.tsx | 910 +++ app/admin/members/page.tsx.backup | 1397 +++++ app/admin/page.tsx | 202 + app/globals.css | 181 + app/layout.tsx | 35 + app/login/loading.tsx | 3 + app/login/page.tsx | 126 + app/page.tsx | 380 ++ app/results/page.tsx | 606 ++ app/vote/page.tsx | 487 ++ components.json | 21 + components/api-test.tsx | 101 + components/auth-guard.tsx | 71 + components/dashboard-header.tsx | 282 + components/theme-provider.tsx | 11 + components/ui/accordion.tsx | 66 + components/ui/alert-dialog.tsx | 157 + components/ui/alert.tsx | 66 + components/ui/aspect-ratio.tsx | 11 + components/ui/avatar.tsx | 53 + components/ui/badge.tsx | 46 + components/ui/breadcrumb.tsx | 109 + components/ui/button.tsx | 59 + components/ui/calendar.tsx | 213 + components/ui/card.tsx | 92 + components/ui/carousel.tsx | 241 + components/ui/chart.tsx | 353 ++ components/ui/checkbox.tsx | 32 + components/ui/collapsible.tsx | 33 + components/ui/command.tsx | 184 + components/ui/context-menu.tsx | 252 + components/ui/dialog.tsx | 143 + components/ui/drawer.tsx | 135 + components/ui/dropdown-menu.tsx | 257 + components/ui/form.tsx | 167 + components/ui/hover-card.tsx | 44 + components/ui/input-otp.tsx | 77 + components/ui/input.tsx | 21 + components/ui/label.tsx | 24 + components/ui/menubar.tsx | 276 + components/ui/navigation-menu.tsx | 168 + components/ui/pagination.tsx | 127 + components/ui/popover.tsx | 48 + components/ui/progress.tsx | 53 + components/ui/radio-group.tsx | 45 + components/ui/resizable.tsx | 56 + components/ui/scroll-area.tsx | 58 + components/ui/select.tsx | 145 + components/ui/separator.tsx | 28 + components/ui/sheet.tsx | 139 + components/ui/sidebar.tsx | 726 +++ components/ui/skeleton.tsx | 13 + components/ui/slider.tsx | 63 + components/ui/sonner.tsx | 25 + components/ui/switch.tsx | 31 + components/ui/table.tsx | 72 + components/ui/tabs.tsx | 66 + components/ui/textarea.tsx | 18 + components/ui/toast.tsx | 129 + components/ui/toaster.tsx | 35 + components/ui/toggle-group.tsx | 73 + components/ui/toggle.tsx | 47 + components/ui/tooltip.tsx | 61 + components/ui/use-mobile.tsx | 19 + components/ui/use-toast.ts | 194 + docker-compose.prod.yml | 91 + docker-compose.yml | 63 + hooks/use-auth.ts | 194 + hooks/use-mobile.ts | 19 + hooks/use-toast.ts | 194 + lib/api-client.ts | 70 + lib/auth.ts | 81 + lib/config.ts | 35 + lib/email.ts | 441 ++ lib/security.ts | 61 + lib/utils.ts | 6 + next.config.mjs | 15 + nginx/nginx.conf | 41 + package-lock.json | 5343 +++++++++++++++++ package.json | 82 + pnpm-lock.yaml | 5 + postcss.config.mjs | 8 + public/favicon.ico | Bin 0 -> 13488 bytes public/images/meti-logo.png | Bin 0 -> 40480 bytes public/images/solar-background.jpg | Bin 0 -> 1174436 bytes public/placeholder-logo.png | Bin 0 -> 568 bytes public/placeholder-logo.svg | 1 + public/placeholder-user.jpg | Bin 0 -> 1635 bytes public/placeholder.jpg | Bin 0 -> 1064 bytes public/placeholder.svg | 1 + styles/globals.css | 123 + tsconfig.json | 27 + 106 files changed, 18736 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 .idea/.gitignore create mode 100644 .idea/e-voting-platform.iml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 AUTH_HEADER_SETUP.md create mode 100644 BACKEND_CONFIG.md create mode 100644 Dockerfile create mode 100644 ENV_SETUP.md create mode 100644 README.md create mode 100644 app/admin/events/[eventId]/candidates/page.tsx create mode 100644 app/admin/events/page.tsx create mode 100644 app/admin/members/loading.tsx create mode 100644 app/admin/members/page.tsx create mode 100644 app/admin/members/page.tsx.backup create mode 100644 app/admin/page.tsx create mode 100644 app/globals.css create mode 100644 app/layout.tsx create mode 100644 app/login/loading.tsx create mode 100644 app/login/page.tsx create mode 100644 app/page.tsx create mode 100644 app/results/page.tsx create mode 100644 app/vote/page.tsx create mode 100644 components.json create mode 100644 components/api-test.tsx create mode 100644 components/auth-guard.tsx create mode 100644 components/dashboard-header.tsx create mode 100644 components/theme-provider.tsx create mode 100644 components/ui/accordion.tsx create mode 100644 components/ui/alert-dialog.tsx create mode 100644 components/ui/alert.tsx create mode 100644 components/ui/aspect-ratio.tsx create mode 100644 components/ui/avatar.tsx create mode 100644 components/ui/badge.tsx create mode 100644 components/ui/breadcrumb.tsx create mode 100644 components/ui/button.tsx create mode 100644 components/ui/calendar.tsx create mode 100644 components/ui/card.tsx create mode 100644 components/ui/carousel.tsx create mode 100644 components/ui/chart.tsx create mode 100644 components/ui/checkbox.tsx create mode 100644 components/ui/collapsible.tsx create mode 100644 components/ui/command.tsx create mode 100644 components/ui/context-menu.tsx create mode 100644 components/ui/dialog.tsx create mode 100644 components/ui/drawer.tsx create mode 100644 components/ui/dropdown-menu.tsx create mode 100644 components/ui/form.tsx create mode 100644 components/ui/hover-card.tsx create mode 100644 components/ui/input-otp.tsx create mode 100644 components/ui/input.tsx create mode 100644 components/ui/label.tsx create mode 100644 components/ui/menubar.tsx create mode 100644 components/ui/navigation-menu.tsx create mode 100644 components/ui/pagination.tsx create mode 100644 components/ui/popover.tsx create mode 100644 components/ui/progress.tsx create mode 100644 components/ui/radio-group.tsx create mode 100644 components/ui/resizable.tsx create mode 100644 components/ui/scroll-area.tsx create mode 100644 components/ui/select.tsx create mode 100644 components/ui/separator.tsx create mode 100644 components/ui/sheet.tsx create mode 100644 components/ui/sidebar.tsx create mode 100644 components/ui/skeleton.tsx create mode 100644 components/ui/slider.tsx create mode 100644 components/ui/sonner.tsx create mode 100644 components/ui/switch.tsx create mode 100644 components/ui/table.tsx create mode 100644 components/ui/tabs.tsx create mode 100644 components/ui/textarea.tsx create mode 100644 components/ui/toast.tsx create mode 100644 components/ui/toaster.tsx create mode 100644 components/ui/toggle-group.tsx create mode 100644 components/ui/toggle.tsx create mode 100644 components/ui/tooltip.tsx create mode 100644 components/ui/use-mobile.tsx create mode 100644 components/ui/use-toast.ts create mode 100644 docker-compose.prod.yml create mode 100644 docker-compose.yml create mode 100644 hooks/use-auth.ts create mode 100644 hooks/use-mobile.ts create mode 100644 hooks/use-toast.ts create mode 100644 lib/api-client.ts create mode 100644 lib/auth.ts create mode 100644 lib/config.ts create mode 100644 lib/email.ts create mode 100644 lib/security.ts create mode 100644 lib/utils.ts create mode 100644 next.config.mjs create mode 100644 nginx/nginx.conf create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 postcss.config.mjs create mode 100644 public/favicon.ico create mode 100644 public/images/meti-logo.png create mode 100644 public/images/solar-background.jpg create mode 100644 public/placeholder-logo.png create mode 100644 public/placeholder-logo.svg create mode 100644 public/placeholder-user.jpg create mode 100644 public/placeholder.jpg create mode 100644 public/placeholder.svg create mode 100644 styles/globals.css create mode 100644 tsconfig.json diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..a39f229 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,63 @@ +# Dependencies +node_modules +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +.pnpm-store + +# Next.js +.next +out +build +dist + +# Testing +coverage +.nyc_output + +# Misc +.DS_Store +*.pem +.vscode +.idea + +# Debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# Local env files +.env*.local +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Vercel +.vercel + +# Typescript +*.tsbuildinfo +next-env.d.ts + +# Git +.git +.gitignore +.gitattributes + +# Docker +Dockerfile +docker-compose*.yml +.dockerignore + +# Documentation +*.md +docs +LICENSE + +# CI/CD +.github +.gitlab-ci.yml +.travis.yml \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f650315 --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules + +# next.js +/.next/ +/out/ + +# production +/build + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..b58b603 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,5 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/.idea/e-voting-platform.iml b/.idea/e-voting-platform.iml new file mode 100644 index 0000000..0c8867d --- /dev/null +++ b/.idea/e-voting-platform.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..26dab84 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/AUTH_HEADER_SETUP.md b/AUTH_HEADER_SETUP.md new file mode 100644 index 0000000..c161a48 --- /dev/null +++ b/AUTH_HEADER_SETUP.md @@ -0,0 +1,102 @@ +# Authorization Header Setup + +## Current Status +✅ **Backend URL**: Fixed to `http://localhost:4002` +❌ **Authorization Header**: Still needs to be set + +## How to Set the Authorization Header + +### Option 1: Edit .env file directly +```bash +# Open the .env file +nano .env +# or +code .env +``` + +Find this line: +```bash +NEXT_PUBLIC_API_AUTH_HEADER= +``` + +And set it to your actual auth header value: +```bash +# Examples: +NEXT_PUBLIC_API_AUTH_HEADER=Bearer your_jwt_token_here +# or +NEXT_PUBLIC_API_AUTH_HEADER=Basic dXNlcjpwYXNz +# or +NEXT_PUBLIC_API_AUTH_HEADER=ApiKey your_api_key_here +# or +NEXT_PUBLIC_API_AUTH_HEADER=your_custom_header_value +``` + +### Option 2: Use sed command +```bash +# Replace with your actual auth header +sed -i '' 's/NEXT_PUBLIC_API_AUTH_HEADER=/NEXT_PUBLIC_API_AUTH_HEADER=Bearer your_token_here/g' .env +``` + +## What Type of Auth Does Your Backend Expect? + +### 1. **Bearer Token** (Most Common) +```bash +NEXT_PUBLIC_API_AUTH_HEADER=Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... +``` + +### 2. **Basic Authentication** +```bash +NEXT_PUBLIC_API_AUTH_HEADER=Basic dXNlcjpwYXNz +``` + +### 3. **API Key** +```bash +NEXT_PUBLIC_API_AUTH_HEADER=ApiKey your_api_key_123 +``` + +### 4. **Custom Header** +```bash +NEXT_PUBLIC_API_AUTH_HEADER=your_custom_value +``` + +## Test Your Backend First + +Before setting the header, test your backend with curl to see what format it expects: + +```bash +# Test without auth header +curl --location 'http://localhost:4002/api/v1/auth/login' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "email": "superadmin@example.com", + "password": "ChangeMe!Super#123" +}' + +# Test with different auth header formats +curl --location 'http://localhost:4002/api/v1/auth/login' \ +--header 'Content-Type: application/json' \ +--header 'Authorization: Bearer test_token' \ +--data-raw '{ + "email": "superadmin@example.com", + "password": "ChangeMe!Super#123" +}' +``` + +## After Setting the Header + +1. **Save the .env file** +2. **Restart your Next.js app**: + ```bash + npm run dev + ``` +3. **Check the browser console** to see if the auth header is now set +4. **Try logging in** again + +## Debug Information + +The app now logs: +- ✅ Backend URL being called +- ✅ Whether auth header is set +- ✅ Response status and data + +Check your browser console (F12 → Console) to see this information. diff --git a/BACKEND_CONFIG.md b/BACKEND_CONFIG.md new file mode 100644 index 0000000..4550159 --- /dev/null +++ b/BACKEND_CONFIG.md @@ -0,0 +1,68 @@ +# Backend Configuration + +## Quick Setup + +If you're having issues with environment variables, you can modify the backend URL directly in the code: + +### 1. Edit `lib/config.ts` + +Find this line in the file: +```typescript +const BACKEND_URL = 'http://localhost:4000' // Change this to your backend URL +``` + +Change it to your actual backend URL, for example: +```typescript +const BACKEND_URL = 'http://localhost:4002' // Your backend URL +``` + +### 2. Set Auth Header + +Also find this line: +```typescript +const AUTH_HEADER = '••••••' // Change this to your actual auth header +``` + +Change it to your actual authorization header value. + +### 3. Restart the Application + +After making changes, restart your Next.js development server: +```bash +npm run dev +``` + +## Current Configuration + +The application is currently configured to call: +- **Login URL**: `http://localhost:4000/api/v1/auth/login` +- **Auth Header**: `••••••` + +## Debug Information + +Check your browser's console (F12 → Console) to see: +- What URL is being called +- Whether the auth header is set +- The response from the backend + +## Alternative: Environment Variables + +If you prefer to use environment variables, create a `.env.local` file in the root directory: + +```bash +NEXT_PUBLIC_API_BASE_URL=http://localhost:4002 +NEXT_PUBLIC_API_AUTH_HEADER=your_actual_auth_header +``` + +## Testing + +Use this curl command to test your backend: +```bash +curl --location 'http://localhost:4002/api/v1/auth/login' \ +--header 'Content-Type: application/json' \ +--header 'Authorization: your_actual_auth_header' \ +--data-raw '{ + "email": "superadmin@example.com", + "password": "ChangeMe!Super#123" +}' +``` diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c86433a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,28 @@ +# Build stage +FROM node:18-alpine AS builder +WORKDIR /app +COPY package*.json ./ +RUN npm ci --legacy-peer-deps +COPY . . +RUN npm run build + +# Production stage +FROM node:18-alpine AS runner +WORKDIR /app + +ENV NODE_ENV=production + +RUN addgroup -g 1001 -S nodejs +RUN adduser -S nextjs -u 1001 + +COPY --from=builder /app/public ./public +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static + +USER nextjs + +EXPOSE 3000 + +ENV PORT=3000 + +CMD ["node", "server.js"] diff --git a/ENV_SETUP.md b/ENV_SETUP.md new file mode 100644 index 0000000..02e7b77 --- /dev/null +++ b/ENV_SETUP.md @@ -0,0 +1,49 @@ +# Environment Setup + +## Required Environment Variables + +Create a `.env.local` file in the root directory with the following variables: + +```bash +# External API Configuration +NEXT_PUBLIC_API_BASE_URL=http://localhost:4000 +NEXT_PUBLIC_API_AUTH_HEADER=•••••• + +# JWT Secret (for local token generation) +JWT_SECRET=your-secret-key-change-this + +# Environment +NODE_ENV=development + +# Base URL for the application +NEXT_PUBLIC_BASE_URL=http://localhost:3000 +``` + +## API Endpoints + +The application expects the following external API endpoints: + +- `POST /api/v1/auth/login` - User authentication +- `POST /api/v1/auth/logout` - User logout +- `GET /api/v1/auth/verify` - Token verification + +## Testing the API + +Use this curl command to test the login endpoint: + +```bash +curl --location 'localhost:4000/api/v1/auth/login' \ +--header 'Content-Type: application/json' \ +--header 'Authorization: ••••••' \ +--data-raw '{ + "email": "superadmin@example.com", + "password": "ChangeMe!Super#123" +}' +``` + +## Notes + +- Replace `••••••` with your actual API authorization header +- The `NEXT_PUBLIC_` prefix makes variables available in the browser +- Keep your `.env.local` file secure and never commit it to version control +- Update the JWT_SECRET to a secure random string in production diff --git a/README.md b/README.md new file mode 100644 index 0000000..4ebc7b9 --- /dev/null +++ b/README.md @@ -0,0 +1,98 @@ +# E-Voting Platform + +A modern e-voting platform built with Next.js and integrated with an external authentication API. + +## Features + +- External API authentication +- Role-based access control (Admin/Voter) +- Modern UI with Tailwind CSS +- Responsive design +- Secure session management + +## Setup + +### Environment Variables + +Create a `.env.local` file in the root directory with the following variables: + +```bash +# External API Configuration +NEXT_PUBLIC_API_BASE_URL=http://localhost:4000 +NEXT_PUBLIC_API_AUTH_HEADER=•••••• + +# JWT Secret (for local token generation) +JWT_SECRET=your-secret-key-change-this + +# Environment +NODE_ENV=development + +# Base URL for the application +NEXT_PUBLIC_BASE_URL=http://localhost:3000 +``` + +### API Endpoints + +The application expects the following external API endpoints: + +- `POST /api/v1/auth/login` - User authentication +- `POST /api/v1/auth/logout` - User logout +- `GET /api/v1/auth/verify` - Token verification + +### Login Credentials + +Use the following credentials for testing: + +```bash +curl --location 'localhost:4000/api/v1/auth/login' \ +--header 'Content-Type: application/json' \ +--header 'Authorization: ••••••' \ +--data-raw '{ + "email": "superadmin@example.com", + "password": "ChangeMe!Super#123" +}' +``` + +## Development + +```bash +# Install dependencies +npm install + +# Run development server +npm run dev + +# Build for production +npm run build + +# Start production server +npm start +``` + +## Architecture + +- **Frontend**: Next.js 15 with React 19 +- **Styling**: Tailwind CSS with shadcn/ui components +- **Authentication**: External API integration with local token storage +- **State Management**: React hooks with localStorage persistence + +## File Structure + +``` +├── app/ # Next.js app directory +│ ├── admin/ # Admin dashboard +│ ├── login/ # Login page +│ ├── vote/ # Voting interface +│ └── api/ # API routes (removed - using external API) +├── components/ # Reusable UI components +├── hooks/ # Custom React hooks +├── lib/ # Utility libraries and configuration +└── public/ # Static assets +``` + +## Security Notes + +- The application now uses an external authentication API +- Local JWT tokens are used for session management +- All sensitive operations are delegated to the external API +- Environment variables should be properly secured in production diff --git a/app/admin/events/[eventId]/candidates/page.tsx b/app/admin/events/[eventId]/candidates/page.tsx new file mode 100644 index 0000000..f19f1c1 --- /dev/null +++ b/app/admin/events/[eventId]/candidates/page.tsx @@ -0,0 +1,513 @@ +"use client" + +import { useState, useEffect } from "react" +import { useParams } from "next/navigation" +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 { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog" +import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@/components/ui/alert-dialog" +import { Plus, Edit, Trash2, ArrowLeft, User, Upload, X } from "lucide-react" +import Link from "next/link" +import Image from "next/image" +import { AuthGuard } from "@/components/auth-guard" +import { useAuth } from "@/hooks/use-auth" +import apiClient from "@/lib/api-client" +import { API_CONFIG } from "@/lib/config" +import { useToast } from "@/hooks/use-toast" + +interface VoteEvent { + id: string + title: string + description: string + start_date: string + end_date: string + is_active: boolean + is_voting_open: boolean +} + +interface Candidate { + id: string + vote_event_id: string + name: string + image_url: string + description: string + created_at: string + updated_at: string +} + +interface CandidateFormData { + name: string + description: string + image_url: string +} + +function CandidateManagementContent() { + const { user, logout } = useAuth() + const { toast } = useToast() + const params = useParams() + const eventId = params.eventId as string + + const [event, setEvent] = useState(null) + const [candidates, setCandidates] = useState([]) + const [loading, setLoading] = useState(true) + const [formData, setFormData] = useState({ + name: "", + description: "", + image_url: "" + }) + const [editingCandidate, setEditingCandidate] = useState(null) + const [isDialogOpen, setIsDialogOpen] = useState(false) + const [submitting, setSubmitting] = useState(false) + const [uploadingImage, setUploadingImage] = useState(false) + const [imageFile, setImageFile] = useState(null) + const [imagePreview, setImagePreview] = useState("") + + useEffect(() => { + if (eventId) { + fetchEventDetails() + fetchCandidates() + } + }, [eventId]) + + const fetchEventDetails = async () => { + try { + const response = await apiClient.get(`${API_CONFIG.ENDPOINTS.VOTE_EVENTS}/${eventId}`) + if (response.data.success) { + setEvent(response.data.data) + } + } catch (error) { + console.error('Error fetching event details:', error) + toast({ + title: "Error", + description: "Failed to fetch event details", + variant: "destructive" + }) + } + } + + const fetchCandidates = async () => { + try { + setLoading(true) + const response = await apiClient.get(`${API_CONFIG.ENDPOINTS.VOTE_EVENTS}/${eventId}/candidates`) + if (response.data.success) { + setCandidates(response.data.data.candidates || []) + } + } catch (error) { + console.error('Error fetching candidates:', error) + toast({ + title: "Error", + description: "Failed to fetch candidates", + variant: "destructive" + }) + } finally { + setLoading(false) + } + } + + const handleImageChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0] + if (file) { + // Validate file type + const validTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp'] + if (!validTypes.includes(file.type)) { + toast({ + title: "Invalid File Type", + description: "Please select a valid image file (JPEG, PNG, GIF, WebP)", + variant: "destructive" + }) + return + } + + // Validate file size (max 5MB) + const maxSize = 5 * 1024 * 1024 // 5MB + if (file.size > maxSize) { + toast({ + title: "File Too Large", + description: "Please select an image smaller than 5MB", + variant: "destructive" + }) + return + } + + setImageFile(file) + const reader = new FileReader() + reader.onloadend = () => { + setImagePreview(reader.result as string) + } + reader.readAsDataURL(file) + } + } + + const uploadImage = async (file: File): Promise => { + const formData = new FormData() + formData.append('file', file) + formData.append('type', file.type) + + try { + const response = await apiClient.post(API_CONFIG.ENDPOINTS.FILES, formData, { + headers: { + 'Content-Type': 'multipart/form-data' + } + }) + + if (response.data.success) { + return response.data.data.url + } else { + throw new Error('Upload failed') + } + } catch (error) { + console.error('Error uploading image:', error) + throw new Error('Failed to upload image') + } + } + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setSubmitting(true) + + try { + let imageUrl = formData.image_url + + // Upload new image if selected + if (imageFile) { + setUploadingImage(true) + try { + imageUrl = await uploadImage(imageFile) + } catch (uploadError) { + setUploadingImage(false) + throw uploadError + } + setUploadingImage(false) + } + + const payload = { + vote_event_id: eventId, + name: formData.name, + image_url: imageUrl, + description: formData.description + } + + if (editingCandidate) { + // Update existing candidate + await apiClient.put(`${API_CONFIG.ENDPOINTS.CANDIDATES}/${editingCandidate.id}`, payload) + toast({ + title: "Success", + description: "Candidate updated successfully" + }) + } else { + // Create new candidate + await apiClient.post(API_CONFIG.ENDPOINTS.CANDIDATES, payload) + toast({ + title: "Success", + description: "Candidate created successfully" + }) + } + + setIsDialogOpen(false) + resetForm() + fetchCandidates() + } catch (error) { + console.error('Error saving candidate:', error) + toast({ + title: "Error", + description: editingCandidate ? "Failed to update candidate" : "Failed to create candidate", + variant: "destructive" + }) + } finally { + setSubmitting(false) + } + } + + const handleEdit = (candidate: Candidate) => { + setEditingCandidate(candidate) + setFormData({ + name: candidate.name, + description: candidate.description, + image_url: candidate.image_url + }) + setImagePreview(candidate.image_url) + setIsDialogOpen(true) + } + + const handleDelete = async (candidateId: string) => { + try { + await apiClient.delete(`${API_CONFIG.ENDPOINTS.CANDIDATES}/${candidateId}`) + toast({ + title: "Success", + description: "Candidate deleted successfully" + }) + fetchCandidates() + } catch (error) { + console.error('Error deleting candidate:', error) + toast({ + title: "Error", + description: "Failed to delete candidate", + variant: "destructive" + }) + } + } + + const resetForm = () => { + setFormData({ + name: "", + description: "", + image_url: "" + }) + setEditingCandidate(null) + setImageFile(null) + setImagePreview("") + setUploadingImage(false) + } + + const removeImage = () => { + setImageFile(null) + setImagePreview("") + setFormData({ ...formData, image_url: "" }) + } + + return ( +
+
+
+
+ + + + METI - New & Renewable Energy +
+

Candidate Management

+ {event && ( +

{event.title}

+ )} +
+
+
+ Welcome, {user?.username || 'Admin'} + +
+
+
+ +
+ {/* Header with Create Button */} +
+
+

Candidates

+

Manage candidates for this voting event

+
+ + + + + + + {editingCandidate ? 'Edit Candidate' : 'Add New Candidate'} + + {editingCandidate ? 'Update the candidate details below.' : 'Fill in the details to add a new candidate.'} + + +
+
+ + setFormData({ ...formData, name: e.target.value })} + placeholder="Enter candidate name" + required + /> +
+ +
+ +