From db0c6c2ccddd8a7c75f96c65da1461460d469cd5 Mon Sep 17 00:00:00 2001 From: Zaki abdullahi Date: Sun, 18 Aug 2024 15:23:42 +0300 Subject: [PATCH 1/4] added admin job management --- package.json | 4 + src/actions/job.action.ts | 72 ++++ src/app/admin/jobs/List.tsx | 43 ++ src/app/admin/jobs/[id]/page.tsx | 26 ++ src/app/admin/jobs/column.tsx | 186 ++++++++ src/app/{ => admin/jobs}/create/page.tsx | 0 src/app/admin/jobs/page.tsx | 22 + src/app/api/admin/jobs/route.ts | 10 + src/components/AlertDailog.tsx | 72 ++++ src/components/job-form.tsx | 469 ++++++++++++--------- src/components/navitem.tsx | 1 + src/components/profile-menu.tsx | 3 - src/components/ui/alert-dialog.tsx | 141 +++++++ src/components/ui/data-table.tsx | 141 +++++++ src/components/ui/table.tsx | 117 +++++ src/config/path.config.ts | 2 +- src/lib/config.ts | 1 + src/lib/session.ts | 6 +- src/lib/validators/jobs.validator.ts | 1 + src/providers/ReactQueryClientProvider.tsx | 14 + src/providers/providers.tsx | 25 +- 21 files changed, 1138 insertions(+), 218 deletions(-) create mode 100644 src/app/admin/jobs/List.tsx create mode 100644 src/app/admin/jobs/[id]/page.tsx create mode 100644 src/app/admin/jobs/column.tsx rename src/app/{ => admin/jobs}/create/page.tsx (100%) create mode 100644 src/app/admin/jobs/page.tsx create mode 100644 src/app/api/admin/jobs/route.ts create mode 100644 src/components/AlertDailog.tsx create mode 100644 src/components/ui/alert-dialog.tsx create mode 100644 src/components/ui/data-table.tsx create mode 100644 src/components/ui/table.tsx create mode 100644 src/lib/config.ts create mode 100644 src/providers/ReactQueryClientProvider.tsx diff --git a/package.json b/package.json index 6e88ac71..97f7d823 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@hookform/resolvers": "^3.9.0", "@prisma/client": "5.18.0", "@radix-ui/react-accordion": "^1.2.0", + "@radix-ui/react-alert-dialog": "^1.1.1", "@radix-ui/react-checkbox": "^1.1.1", "@radix-ui/react-dialog": "^1.1.1", "@radix-ui/react-dropdown-menu": "^2.1.1", @@ -32,6 +33,9 @@ "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-switch": "^1.1.0", "@radix-ui/react-toast": "^1.2.1", + "@tanstack/react-query": "^5.51.23", + "@tanstack/react-table": "^8.20.1", + "axios": "^1.7.4", "bcryptjs": "^2.4.3", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", diff --git a/src/actions/job.action.ts b/src/actions/job.action.ts index 0d9cb930..12294d86 100644 --- a/src/actions/job.action.ts +++ b/src/actions/job.action.ts @@ -35,6 +35,7 @@ export const createJob = withSession< hasSalaryRange, maxSalary, minSalary, + currency, } = result; await prisma.job.create({ data: { @@ -48,6 +49,7 @@ export const createJob = withSession< isVerifiedJob, location, workMode, + currency, }, }); const message = isVerifiedJob @@ -146,3 +148,73 @@ export const jobFilterQuery = async (queries: JobQuerySchemaType) => { workmode?.map((mode) => searchParams.append('workmode', mode)); redirect(`/jobs?${searchParams.toString()}`); }; + +export const updateJob = async (data: JobPostSchemaType, id: string) => { + const result = JobPostSchema.parse(data); + const { + companyName, + location, + title, + workMode, + description, + hasSalaryRange, + maxSalary, + minSalary, + currency, + } = result; + + try { + await prisma.job.update({ + where: { + id: id, + }, + data: { + title, + description, + companyName, + hasSalaryRange, + minSalary, + maxSalary, + location, + workMode, + currency, + }, + }); + + return { + message: 'succeffuly updated', + status: 200, + name: 'success', + }; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (error: any) { + return { + message: 'Error updated', + status: 500, + name: 'Error', + }; + } +}; + +export const deleteJob = async (id: string) => { + try { + await prisma.job.delete({ + where: { + id: id, + }, + }); + + return { + message: 'succeffuly deleted ', + status: 200, + name: 'success', + }; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (error: any) { + return { + message: 'Error deleting Job', + status: 500, + name: 'Error', + }; + } +}; diff --git a/src/app/admin/jobs/List.tsx b/src/app/admin/jobs/List.tsx new file mode 100644 index 00000000..1fa805e2 --- /dev/null +++ b/src/app/admin/jobs/List.tsx @@ -0,0 +1,43 @@ +'use client'; +import React from 'react'; +import { useQuery } from '@tanstack/react-query'; +import axios from 'axios'; +import { API } from '@/lib/config'; + +import { DataTable } from '@/components/ui/data-table'; +import { columns } from './column'; +import { Button } from '@/components/ui/button'; +import { useRouter } from 'next/navigation'; + +const List = () => { + const router = useRouter(); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { data, isError, isLoading } = useQuery({ + queryKey: ['jobs'], + queryFn: () => axios.get(`${API}/admin/jobs`).then((res) => res.data), + staleTime: 1000 * 60, + retry: 3, + }); + + if (isLoading) return

...loading Jops

; + + // eslint-disable-next-line no-console + console.log('Product data', data); + + return ( +
+
+ +
+ +
+ ); +}; + +export default List; diff --git a/src/app/admin/jobs/[id]/page.tsx b/src/app/admin/jobs/[id]/page.tsx new file mode 100644 index 00000000..3389cd7d --- /dev/null +++ b/src/app/admin/jobs/[id]/page.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import prisma from '@/config/prisma.config'; + +import { notFound } from 'next/navigation'; +import PostJobForm from '@/components/job-form'; + +const UpdateJoPage = async ({ params }: { params: { id: string } }) => { + let job; + + try { + job = await prisma?.job.findUnique({ where: { id: params.id } }); + + if (!job) notFound(); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (err) { + notFound(); + } + + return ( +
+ +
+ ); +}; + +export default UpdateJoPage; diff --git a/src/app/admin/jobs/column.tsx b/src/app/admin/jobs/column.tsx new file mode 100644 index 00000000..786f8dd1 --- /dev/null +++ b/src/app/admin/jobs/column.tsx @@ -0,0 +1,186 @@ +'use client'; +import { WorkMode, Currency } from '@prisma/client'; +import { ColumnDef } from '@tanstack/react-table'; + +import { ArrowUpDown } from 'lucide-react'; + +import { MoreHorizontal } from 'lucide-react'; + +import { Button } from '@/components/ui/button'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { useRouter } from 'next/navigation'; +import AlertDailog from '@/components/AlertDailog'; +// This type is used to define the shape of our data. +// You can use a Zod schema here if you want. +export type Job = { + id: string; + userId: string; + title: string; + description: string; + companyName: 'Tech Corp'; + workMode: typeof WorkMode.remote; + currency: typeof Currency.USD; + location: string; + hasSalaryRange: boolean; + minSalary: number; + maxSalary: number; + isVerifiedJob: Boolean; +}; + +export const columns: ColumnDef[] = [ + { + accessorKey: 'id', + header: ({ column }) => { + return ( + + ); + }, + }, + { + accessorKey: 'title', + header: ({ column }) => { + return ( + + ); + }, + }, + { + accessorKey: 'companyName', + header: ({ column }) => { + return ( + + ); + }, + }, + { + accessorKey: 'description', + header: ({ column }) => { + return ( + + ); + }, + }, + { + accessorKey: 'location', + header: ({ column }) => { + return ( + + ); + }, + }, + { + accessorKey: 'minSalary', + header: ({ column }) => { + return ( + + ); + }, + }, + { + accessorKey: 'maxSalary', + header: ({ column }) => { + return ( + + ); + }, + }, + + // { + // id: 'actions', + // header: 'Actions', + // cell: ({ row }) => { + // const jobInfo = row.original; + + // const router = useRouter(); + + // return ( + //
+ // + // {/* */} + //
+ // ); + // }, + // }, + { + id: 'actions', + cell: ({ row }) => { + const jobInfo = row.original; + // eslint-disable-next-line react-hooks/rules-of-hooks + const router = useRouter(); + + return ( + + + + + + Actions + + + router.push(`jobs/${jobInfo.id}`)}> + Edit + + + + + ); + }, + }, +]; diff --git a/src/app/create/page.tsx b/src/app/admin/jobs/create/page.tsx similarity index 100% rename from src/app/create/page.tsx rename to src/app/admin/jobs/create/page.tsx diff --git a/src/app/admin/jobs/page.tsx b/src/app/admin/jobs/page.tsx new file mode 100644 index 00000000..85e567e7 --- /dev/null +++ b/src/app/admin/jobs/page.tsx @@ -0,0 +1,22 @@ +import { getServerSession } from 'next-auth'; +import List from './List'; +import { options } from '@/lib/auth'; +import { redirect } from 'next/navigation'; + +const JobsPage = async () => { + const session = await getServerSession(options); + + // eslint-disable-next-line no-console + console.log('session', session); + + if (session?.user?.role !== 'ADMIN') { + redirect('/'); + } + return ( +
+ +
+ ); +}; + +export default JobsPage; diff --git a/src/app/api/admin/jobs/route.ts b/src/app/api/admin/jobs/route.ts new file mode 100644 index 00000000..f588f3f4 --- /dev/null +++ b/src/app/api/admin/jobs/route.ts @@ -0,0 +1,10 @@ +import prisma from '@/config/prisma.config'; +import { NextRequest, NextResponse } from 'next/server'; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export async function GET(request: NextRequest) { + // return NextResponse.json("erroor", { status: 500 }) + + const products = await prisma.job.findMany({}); + return NextResponse.json(products, { status: 200 }); +} diff --git a/src/components/AlertDailog.tsx b/src/components/AlertDailog.tsx new file mode 100644 index 00000000..0de68662 --- /dev/null +++ b/src/components/AlertDailog.tsx @@ -0,0 +1,72 @@ +import { useState } from 'react'; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from '@/components/ui/alert-dialog'; +import { Button } from '@/components/ui/button'; +import { useRouter } from 'next/navigation'; + +import { useQueryClient } from '@tanstack/react-query'; +import { useToast } from './ui/use-toast'; +import { deleteJob } from '@/actions/job.action'; + +const AlertDailog = ({ id }: { id: string }) => { + const [loading, setLoading] = useState(false); + const router = useRouter(); + const { toast } = useToast(); + + const queryClient = useQueryClient(); + const handleDelete = async () => { + try { + setLoading(true); + + const response = await deleteJob(id); + toast({ + title: response.message, + variant: 'success', + }); + queryClient.invalidateQueries({ queryKey: ['jobs'] }); + setLoading(false); + + router.push('/admin/jobs'); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (error: any) { + setLoading(false); + + toast({ title: "Something Won't happen", variant: 'destructive' }); + } + }; + + return ( + + + + + + + Are you absolutely sure? + + This action cannot be undone. This will permanently delete Job and + remove your data from our servers. + + + + Cancel + Continue + + + + ); +}; + +export default AlertDailog; diff --git a/src/components/job-form.tsx b/src/components/job-form.tsx index 0fd89541..f34db3c0 100644 --- a/src/components/job-form.tsx +++ b/src/components/job-form.tsx @@ -1,5 +1,7 @@ 'use client'; -import { createJob } from '@/actions/job.action'; +import { createJob, updateJob } from '@/actions/job.action'; + +import { useRouter } from 'next/navigation'; import { Form, FormControl, @@ -29,42 +31,76 @@ import { Label } from './ui/label'; import { Switch } from './ui/switch'; import { useToast } from './ui/use-toast'; import { filters } from '@/lib/constant/jobs.constant'; -const PostJobForm = () => { +import { Job } from '@prisma/client'; +import { useQueryClient } from '@tanstack/react-query'; +const PostJobForm = ({ job }: { job?: Job }) => { + const queryClient = useQueryClient(); const { toast } = useToast(); + const router = useRouter(); const form = useForm({ resolver: zodResolver(JobPostSchema), defaultValues: { - title: '', - description: '', - companyName: '', - location: '', - hasSalaryRange: false, - minSalary: 0, - maxSalary: 0, + title: job?.title || '', + description: job?.description || '', + companyName: job?.companyName || '', + location: job?.location || '', + hasSalaryRange: job?.hasSalaryRange || false, + minSalary: job?.minSalary || 0, + maxSalary: job?.maxSalary || 0, + currency: job?.currency || 'INR', }, }); const handleFormSubmit = async (data: JobPostSchemaType) => { - try { - const response = await createJob(data); + if (!job) { + try { + const response = await createJob(data); - if (!response.status) { - return toast({ - title: response.name || 'Something went wrong', - description: response.message || 'Internal server error', + if (!response.status) { + return toast({ + title: response.name || 'Something went wrong', + description: response.message || 'Internal server error', + variant: 'destructive', + }); + } + toast({ + title: response.message, + variant: 'success', + }); + form.reset(form.formState.defaultValues); + queryClient.invalidateQueries({ queryKey: ['jobs'] }); + router.push('/admin/jobs'); + } catch (_error) { + toast({ + title: 'Something went wrong will creating job', + description: 'Internal server error', + variant: 'destructive', + }); + } + } else { + try { + const response = await updateJob(data, job.id); + + if (!response.status) { + return toast({ + title: response.name || 'Something went wrong', + description: response.message || 'Internal server error', + variant: 'destructive', + }); + } + toast({ + title: response.message, + variant: 'success', + }); + form.reset(form.formState.defaultValues); + queryClient.invalidateQueries({ queryKey: ['jobs'] }); + router.push('/admin/jobs'); + } catch (_error) { + toast({ + title: 'Something went wrong will creating job', + description: 'Internal server error', variant: 'destructive', }); } - toast({ - title: response.message, - variant: 'success', - }); - form.reset(form.formState.defaultValues); - } catch (_error) { - toast({ - title: 'Something went wrong will creating job', - description: 'Internal server error', - variant: 'destructive', - }); } }; const watchHasSalaryRange = form.watch('hasSalaryRange'); @@ -80,192 +116,221 @@ const PostJobForm = () => {
-
- ( - - - Job Title - - - - - - - )} - /> -
+
+
+
+ ( + + + Job Title + + + + + + + )} + /> +
-
- ( - - - Description - - -