Skip to content

Commit

Permalink
Add admin dashboard functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
JohanMejia77 committed May 15, 2024
1 parent 8eada3e commit 201aa03
Show file tree
Hide file tree
Showing 29 changed files with 1,961 additions and 9 deletions.
602 changes: 600 additions & 2 deletions package-lock.json

Large diffs are not rendered by default.

11 changes: 10 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@
"lint": "next lint"
},
"dependencies": {
"@hookform/resolvers": "^3.3.4",
"@radix-ui/react-alert-dialog": "^1.0.5",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-slot": "^1.0.2",
"@types/react-star-ratings": "^2.3.3",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
Expand All @@ -19,12 +24,16 @@
"react": "^18",
"react-dom": "^18",
"react-fast-marquee": "^1.6.4",
"react-hook-form": "^7.51.3",
"react-icons": "^5.0.1",
"react-image-gallery": "^1.3.0",
"react-star-ratings": "^2.3.0",
"react-svg": "^16.1.33",
"sonner": "^1.4.41",
"tailwind-merge": "^2.2.1",
"tailwindcss-animate": "^1.0.7"
"tailwindcss-animate": "^1.0.7",
"zod": "^3.23.5",
"zustand": "^4.5.2"
},
"devDependencies": {
"@types/node": "^20",
Expand Down
24 changes: 24 additions & 0 deletions src/actions/admin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"use server";

import { REVIEWS_API } from "@/lib/constants";

export const login = async (email: string, password: string) => {
const response = await fetch(`${REVIEWS_API}/admin-users/logIn`, {
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ thisUser: email, pass: password }),
method: "POST",
});

const data = await response.json();

if (data.statusCode === 401) {
return {
error: "Acceso no autorizado",
};
}
return {
token: data.access_token,
};
};
79 changes: 77 additions & 2 deletions src/actions/reviews.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,88 @@
"use server";

import { REVIEWS_API } from "@/lib/constants";
import { reviewSchema } from "@/app/(admin)/atc24$rw/admin/schema";
import { z } from "zod";
import { revalidatePath } from "next/cache";
import { Review } from "@/types";

export const getReviews = async () => {
export const getReviews = async (): Promise<Review[]> => {
try {
const response = await fetch(REVIEWS_API);
const response = await fetch(`${REVIEWS_API}/reviews`);
const { data } = await response.json();
return data;
} catch (_) {
return [];
}
};
export const createReview = async (
review: z.infer<typeof reviewSchema>,
token: string
) => {
const newReview = {
review: review.review,
rating: String(review.rating),
user: review.user,
date: new Date(),
};

const response = await fetch(`${REVIEWS_API}/reviews`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify(newReview),
});
if (!response.ok) {
return {
error: "Su sesión ha expirado, ingrese nuevamente",
};
}

revalidatePath("atc24$rw/admin");
revalidatePath("/");
return {
success: "Se creó una nueva reseña",
};
};
export const deleteReview = async (id: string, token: string) => {
const response = await fetch(`${REVIEWS_API}/reviews/${id}`, {
method: "DELETE",
headers: {
Authorization: `Bearer ${token}`,
},
});
if (!response.ok) {
return {
error: "Su sesión ha expirado, ingrese nuevamente",
};
}
revalidatePath("atc24$rw/admin");
revalidatePath("/");
return {
success: "Se eliminó la reseña",
};
};
export const updateReview = async (updatedReview: Review, token: string) => {
const response = await fetch(`${REVIEWS_API}/reviews`, {
method: "PATCH",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify(updatedReview),
});
if (!response.ok) {
return {
error: "Su sesión ha expirado, ingrese nuevamente",
};
}

revalidatePath("atc24$rw/admin");
revalidatePath("/");

return {
success: "Se ha actualizado la reseña",
};
};
34 changes: 34 additions & 0 deletions src/app/(admin)/atc24$rw/admin/components/DeleteAlert.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";

interface DeleteAlertProps {
children: React.ReactNode;
onDelete: () => void;
}

export const DeleteAlert = ({ children, onDelete }: DeleteAlertProps) => {
return (
<AlertDialog>
<AlertDialogTrigger>{children}</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
¿Esta seguro de eliminar esta reseña?
</AlertDialogTitle>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancelar</AlertDialogCancel>
<AlertDialogAction onClick={onDelete}>Confirmar</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};
26 changes: 26 additions & 0 deletions src/app/(admin)/atc24$rw/admin/components/FormModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"use client";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { useReviewFormModal } from "@/store/useReviewFormModal";
import { ReviewForm } from "./ReviewForm";

export const FormModal = () => {
const { isOpen, onClose, defaultValues } = useReviewFormModal();

const title = !defaultValues?.id ? "Añadir reseña" : "Editar reseña";

return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent>
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
</DialogHeader>
<ReviewForm review={defaultValues} />
</DialogContent>
</Dialog>
);
};
21 changes: 21 additions & 0 deletions src/app/(admin)/atc24$rw/admin/components/ModalProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"use client";
import { useEffect, useState } from "react";
import { FormModal } from "./FormModal";

export const ModalProvider = () => {
const [isMounted, setIsMounted] = useState(false);

useEffect(() => {
setIsMounted(true);
}, []);

if (!isMounted) {
return null;
}

return (
<>
<FormModal />
</>
);
};
155 changes: 155 additions & 0 deletions src/app/(admin)/atc24$rw/admin/components/ReviewForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
"use client";
import { useForm } from "react-hook-form";
import { useRouter } from "next/navigation";
import { useReviewFormModal } from "@/store/useReviewFormModal";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { reviewSchema } from "../schema";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { useTransition } from "react";
import { Input } from "@/components/ui/input";
import { createReview, updateReview } from "@/actions/reviews";
import { Review } from "@/types";
import { Button } from "@/components/ui/button";
import { ImSpinner2 } from "react-icons/im";
import { toast } from "sonner";
import { Textarea } from "@/components/ui/textarea";

interface ReviewFormProps {
review?: Review;
}

export const ReviewForm = ({ review }: ReviewFormProps) => {
const [isPending, startTransition] = useTransition();
const { onClose } = useReviewFormModal();
const router = useRouter();

const form = useForm<z.infer<typeof reviewSchema>>({
resolver: zodResolver(reviewSchema),
defaultValues: {
review: review?.review || "",
rating: review?.rating || "5",
user: review?.user || "",
},
});

const handleCreate = (
values: z.infer<typeof reviewSchema>,
token: string
) => {
startTransition(() => {
createReview(values, token)
.then((data) => {
if (data.success) {
toast.success(data.success);
onClose();
}
if (data.error) {
toast.error(data.error);
router.push("/atc24$rw");
}
})
.catch(() => toast.error("Ocurrió un error"));
});
};

const handleUpdate = (review: Review, token: string) => {
startTransition(() => {
updateReview(review, token)
.then((data) => {
if (data.success) {
toast.success(data.success);
onClose();
}
if (data.error) {
toast.error(data.error);
router.push("/atc24$rw");
}
})
.catch(() => toast.error("Ocurrió un error"));
});
};

const onSubmit = (values: z.infer<typeof reviewSchema>) => {
const token = sessionStorage.getItem("token") || "";
if (!review) {
handleCreate(values, token);
} else {
const updatedReview = {
id: review.id,
...values
}

handleUpdate(updatedReview, token);
}
};

return (
<Form {...form}>
<form
className="space-y-8 flex flex-col items-center"
onSubmit={form.handleSubmit(onSubmit)}
>
<FormField
control={form.control}
name="review"
render={({ field }) => (
<FormItem className="w-full">
<FormLabel className="font-bold">Texto</FormLabel>
<FormControl>
<Textarea
{...field}
placeholder="Texto de la reseña"
className="max-w-full h-[150px] resize-none"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="rating"
render={({ field }) => (
<FormItem className="w-full">
<FormLabel>Calificación</FormLabel>
<FormControl>
<Input type="number" max={5} min={1} {...field} step={0.5}/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="user"
render={({ field }) => (
<FormItem className="w-full">
<FormLabel>Nombre del usuario</FormLabel>
<FormControl>
<Input placeholder="Nombre del usuario" {...field} />
</FormControl>
</FormItem>
)}
/>
<Button
className="w-full bg-primary-lm hover:bg-red-600"
disabled={isPending}
>
{!isPending ? (
review ? "Actualizar reseña" : "Registrar reseña"
) : (
<ImSpinner2 size={20} className="animate-spin h-5 w-5" />
)}
</Button>
</form>
</Form>
);
};
Loading

0 comments on commit 201aa03

Please sign in to comment.