diff --git a/app/admin/dashboard/jobs/page.tsx b/app/admin/dashboard/jobs/page.tsx new file mode 100644 index 0000000..afbbe4b --- /dev/null +++ b/app/admin/dashboard/jobs/page.tsx @@ -0,0 +1,522 @@ +'use client'; + +import { useEffect, useMemo, useState } from 'react'; +import { Plus, Pencil, Trash2 } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Textarea } from '@/components/ui/textarea'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@/components/ui/dialog'; +import { Badge } from '@/components/ui/badge'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; + +type JobDoc = { + _id: string; + title: string; + company: string; + location?: string; + workplaceType?: string; + employmentType?: string; + audience?: string; + description: string; + requirements?: string; + applyUrl?: string; + applyEmail?: string; + deadline?: string; + tags?: string[]; + isActive: boolean; + createdAt?: string; + updatedAt?: string; +}; + +type JobFormData = { + title: string; + company: string; + location: string; + workplaceType: 'Remote' | 'Hybrid' | 'Onsite'; + employmentType: 'Internship' | 'Full-time' | 'Part-time' | 'Contract'; + audience: 'Students' | 'Professionals' | 'Both'; + description: string; + requirements: string; + applyUrl: string; + applyEmail: string; + deadline: string; // YYYY-MM-DD + tags: string; // comma-separated + isActive: boolean; +}; + +const defaultForm: JobFormData = { + title: '', + company: '', + location: '', + workplaceType: 'Remote', + employmentType: 'Full-time', + audience: 'Both', + description: '', + requirements: '', + applyUrl: '', + applyEmail: '', + deadline: '', + tags: '', + isActive: true, +}; + +function toDateInputValue(d?: string) { + if (!d) return ''; + const date = new Date(d); + if (Number.isNaN(date.getTime())) return ''; + return date.toISOString().slice(0, 10); +} + +export default function JobsAdminPage() { + const [jobs, setJobs] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(''); + + const [dialogOpen, setDialogOpen] = useState(false); + const [editingId, setEditingId] = useState(null); + const [formData, setFormData] = useState(defaultForm); + + const [search, setSearch] = useState(''); + const [statusFilter, setStatusFilter] = useState<'all' | 'active' | 'inactive'>('all'); + + const fetchJobs = async () => { + try { + setError(''); + setLoading(true); + const res = await fetch('/api/admin/jobs', { cache: 'no-store' }); + const data = await res.json(); + if (!res.ok) throw new Error(data?.error || 'Failed to fetch jobs'); + setJobs(Array.isArray(data) ? data : []); + } catch (e: any) { + setError(e?.message || 'Failed to load jobs'); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchJobs(); + }, []); + + const filteredJobs = useMemo(() => { + const q = search.trim().toLowerCase(); + return jobs.filter((j) => { + if (statusFilter === 'active' && !j.isActive) return false; + if (statusFilter === 'inactive' && j.isActive) return false; + if (!q) return true; + return ( + j.title?.toLowerCase().includes(q) || + j.company?.toLowerCase().includes(q) || + (j.location || '').toLowerCase().includes(q) + ); + }); + }, [jobs, search, statusFilter]); + + const resetForm = () => { + setEditingId(null); + setFormData(defaultForm); + setError(''); + }; + + const openCreate = () => { + resetForm(); + setDialogOpen(true); + }; + + const openEdit = (job: JobDoc) => { + setEditingId(job._id); + setFormData({ + title: job.title || '', + company: job.company || '', + location: job.location || '', + workplaceType: (job.workplaceType as any) || 'Remote', + employmentType: (job.employmentType as any) || 'Full-time', + audience: (job.audience as any) || 'Both', + description: job.description || '', + requirements: job.requirements || '', + applyUrl: job.applyUrl || '', + applyEmail: job.applyEmail || '', + deadline: toDateInputValue(job.deadline), + tags: Array.isArray(job.tags) ? job.tags.join(', ') : '', + isActive: !!job.isActive, + }); + setError(''); + setDialogOpen(true); + }; + + const validateForm = () => { + if (!formData.title.trim()) return 'Title is required'; + if (!formData.company.trim()) return 'Company is required'; + if (!formData.description.trim()) return 'Description is required'; + if (!formData.applyUrl.trim() && !formData.applyEmail.trim()) return 'Provide apply URL or apply email'; + return ''; + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + const msg = validateForm(); + if (msg) { + setError(msg); + return; + } + + try { + setError(''); + const payload = { + ...formData, + deadline: formData.deadline ? new Date(formData.deadline).toISOString() : undefined, + tags: formData.tags + .split(',') + .map((t) => t.trim()) + .filter(Boolean), + }; + + const url = editingId ? `/api/admin/jobs?id=${editingId}` : '/api/admin/jobs'; + const res = await fetch(url, { + method: editingId ? 'PUT' : 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + }); + const data = await res.json(); + if (!res.ok) throw new Error(data?.error || (editingId ? 'Failed to update job' : 'Failed to create job')); + + setDialogOpen(false); + resetForm(); + await fetchJobs(); + } catch (e: any) { + setError(e?.message || 'Failed to save job'); + } + }; + + const handleDelete = async (id: string) => { + if (!confirm('Delete this job?')) return; + try { + setError(''); + const res = await fetch(`/api/admin/jobs?id=${id}`, { method: 'DELETE' }); + const data = await res.json(); + if (!res.ok) throw new Error(data?.error || 'Failed to delete job'); + setJobs((prev) => prev.filter((j) => j._id !== id)); + } catch (e: any) { + setError(e?.message || 'Failed to delete job'); + } + }; + + if (loading) { + return ( +
+
Loading jobs...
+
+ ); + } + + return ( +
+
+
+

Jobs

+

Create, edit, and delete careers listings.

+
+ + { + setDialogOpen(open); + if (!open) resetForm(); + }} + > + + + + + + {editingId ? 'Edit Job' : 'Create Job'} + Fill in the job details. At least one apply method is required. + + +
+
+ {error && ( +
+ {error} +
+ )} + +
+
+ + setFormData({ ...formData, title: e.target.value })} + required + /> +
+
+ + setFormData({ ...formData, company: e.target.value })} + required + /> +
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ + setFormData({ ...formData, location: e.target.value })} + placeholder="e.g. Lahore, PK" + /> +
+
+ + setFormData({ ...formData, deadline: e.target.value })} + /> +
+
+ +
+ +