diff --git a/apps/admin/app/(main)/jobs/[jobId]/page.tsx b/apps/admin/app/(main)/jobs/[jobId]/page.tsx index cb6e693..f77a7e2 100644 --- a/apps/admin/app/(main)/jobs/[jobId]/page.tsx +++ b/apps/admin/app/(main)/jobs/[jobId]/page.tsx @@ -15,7 +15,7 @@ export default async function JobDetailPage({ params }: JobPageProps) { if (isNaN(jobId)) notFound(); const jobRes = await db.select().from(jobs).where(eq(jobs.id, jobId)).limit(1); - if (jobRes.length === 0) notFound(); + if (jobRes.length === 0 || !jobRes[0]) notFound(); const job = jobRes[0]; const companyRes = await db.select().from(companies).where(eq(companies.id, job.companyId)).limit(1); diff --git a/apps/admin/app/(main)/jobs/new/actions.ts b/apps/admin/app/(main)/jobs/new/actions.ts new file mode 100644 index 0000000..e721645 --- /dev/null +++ b/apps/admin/app/(main)/jobs/new/actions.ts @@ -0,0 +1,53 @@ +'use server'; +import { db, companies, jobs } from '@workspace/db'; + +export async function createJob(formData: FormData) { + const companyIdRaw = formData.get('companyId'); + const companyId = companyIdRaw ? Number(companyIdRaw) : undefined; + const title = String(formData.get('title') ?? '').trim(); + const link = String(formData.get('link') ?? '').trim(); + const description = String(formData.get('description') ?? '').trim() || 'N/A'; + const location = String(formData.get('location') ?? '').trim() || 'N/A'; + const imageURL = + String(formData.get('imageURL') ?? '').trim() || 'https://via.placeholder.com/100x100?text=Job'; + const salary = String(formData.get('salary') ?? '').trim() || 'N/A'; + const deadlineRaw = formData.get('applicationDeadline'); + const applicationDeadline = deadlineRaw ? new Date(String(deadlineRaw)) : new Date(); + const minCGPA = formData.get('minCGPA') !== null ? String(formData.get('minCGPA')) : '0'; + const minSSC = formData.get('minSSC') !== null ? String(formData.get('minSSC')) : '0'; + const minHSC = formData.get('minHSC') !== null ? String(formData.get('minHSC')) : '0'; + const allowDeadKT = formData.get('allowDeadKT') === 'on' || formData.get('allowDeadKT') === 'true'; + const allowLiveKT = formData.get('allowLiveKT') === 'on' || formData.get('allowLiveKT') === 'true'; + + if (!companyId || !title) return { error: 'Company and title are required.' }; + + await db.insert(jobs).values({ + companyId, + title, + link, + description, + location, + imageURL, + salary, + applicationDeadline, + active: true, + minCGPA, + minSSC, + minHSC, + allowDeadKT, + allowLiveKT, + }); + return { success: true }; +} + +export async function createCompany(formData: FormData) { + const name = String(formData.get('name') ?? '').trim(); + const email = String(formData.get('email') ?? '').trim(); + const link = String(formData.get('link') ?? '').trim(); + const description = String(formData.get('description') ?? '').trim(); + const imageURL = String(formData.get('imageURL') ?? '').trim() || 'https://via.placeholder.com/100x100?text=Company'; + if (!name || !email || !link || !description) return { error: 'All fields are required.' }; + const [inserted] = await db.insert(companies).values({ name, email, link, description, imageURL }).returning(); + if (!inserted) return { error: 'Failed to add company.' }; + return { success: true, company: { id: inserted.id, name: inserted.name } }; +} diff --git a/apps/admin/app/(main)/jobs/new/loading.tsx b/apps/admin/app/(main)/jobs/new/loading.tsx new file mode 100644 index 0000000..3e54555 --- /dev/null +++ b/apps/admin/app/(main)/jobs/new/loading.tsx @@ -0,0 +1,31 @@ +import { Skeleton } from '@workspace/ui/components/skeleton'; + +export default function Loading() { + return ( +
+
+
+ +
+ + + + + + +
+ + + +
+
+ + +
+ +
+
+
+
+ ); +} diff --git a/apps/admin/app/(main)/jobs/new/new-job-form.tsx b/apps/admin/app/(main)/jobs/new/new-job-form.tsx new file mode 100644 index 0000000..a28e862 --- /dev/null +++ b/apps/admin/app/(main)/jobs/new/new-job-form.tsx @@ -0,0 +1,366 @@ +'use client'; +import { useState, useTransition } from 'react'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { Input } from '@workspace/ui/components/input'; +import { Textarea } from '@workspace/ui/components/textarea'; +import { Button } from '@workspace/ui/components/button'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@workspace/ui/components/form'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@workspace/ui/components/select'; +import { Card, CardContent, CardHeader, CardTitle } from '@workspace/ui/components/card'; +import { jobSchema, JobFormData } from './schema'; +import { createJob, createCompany } from './actions'; +import { Popover, PopoverTrigger, PopoverContent } from '@workspace/ui/components/popover'; +import { Calendar } from '@workspace/ui/components/calendar'; +import { format } from 'date-fns'; +import { Calendar as CalendarIcon } from 'lucide-react'; +import { cn } from '@workspace/ui/lib/utils'; + +function NewJobForm({ companies }: { companies: { id: number; name: string }[] }) { + const [success, setSuccess] = useState(false); + const [error, setError] = useState(null); + const [isPending, startTransition] = useTransition(); + const [showModal, setShowModal] = useState(false); + const [addingCompany, setAddingCompany] = useState(false); + const [companyList, setCompanyList] = useState(companies); + const [newCompanyName, setNewCompanyName] = useState(''); + const [newCompanyEmail, setNewCompanyEmail] = useState(''); + const [newCompanyLink, setNewCompanyLink] = useState(''); + const [newCompanyDescription, setNewCompanyDescription] = useState(''); + const [newCompanyImageURL, setNewCompanyImageURL] = useState(''); + const [companyError, setCompanyError] = useState(null); + const form = useForm({ + resolver: zodResolver(jobSchema), + defaultValues: { + companyId: companies[0]?.id ?? 0, + title: '', + link: '', + description: '', + location: '', + imageURL: '', + salary: '', + applicationDeadline: new Date(), + minCGPA: 0, + minSSC: 0, + minHSC: 0, + allowDeadKT: true, + allowLiveKT: true, + }, + }); + + async function handleSubmit(formData: FormData) { + setError(null); + setSuccess(false); + startTransition(async () => { + const result = await createJob(formData); + if (result?.success) { + setSuccess(true); + form.reset(form.formState.defaultValues); + } else { + setError(result?.error || 'Failed to create job'); + } + }); + } + + async function handleAddCompany(e: React.FormEvent) { + e.preventDefault(); + setCompanyError(null); + if (!newCompanyName.trim() || !newCompanyEmail.trim() || !newCompanyLink.trim() || !newCompanyDescription.trim()) return; + setAddingCompany(true); + const formData = new FormData(); + formData.append('name', newCompanyName.trim()); + formData.append('email', newCompanyEmail.trim()); + formData.append('link', newCompanyLink.trim()); + formData.append('description', newCompanyDescription.trim()); + formData.append('imageURL', newCompanyImageURL.trim()); + const result = await createCompany(formData); + if (result?.success && result.company) { + setCompanyList((prev) => [...prev, result.company]); + form.setValue('companyId', result.company.id); + setNewCompanyName(''); + setNewCompanyEmail(''); + setNewCompanyLink(''); + setNewCompanyDescription(''); + setNewCompanyImageURL(''); + setShowModal(false); + } else { + setCompanyError(result?.error || 'Failed to add company'); + } + setAddingCompany(false); + } + + return ( +
+
+ + + Create a New Job + + +
+ + ( + + Company * + + + + )} + /> + ( + + Title * + + + + + + )} + /> + ( + + Job Link * + + + + + + )} + /> + ( + + Description * + +