87 | >(({ className, ...props }, ref) => (
88 | [role=checkbox]]:translate-y-[2px]",
92 | className
93 | )}
94 | {...props}
95 | />
96 | ))
97 | TableCell.displayName = "TableCell"
98 |
99 | const TableCaption = React.forwardRef<
100 | HTMLTableCaptionElement,
101 | React.HTMLAttributes
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | TableCaption.displayName = "TableCaption"
110 |
111 | export {
112 | Table,
113 | TableHeader,
114 | TableBody,
115 | TableFooter,
116 | TableHead,
117 | TableRow,
118 | TableCell,
119 | TableCaption,
120 | }
121 |
--------------------------------------------------------------------------------
/app/(public)/certification/_components/CertificationDetailContainer.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { getSingleCertificationAction } from "@/actions/certification.actions";
3 | import { CertificationMac } from "@/components/CertificationMac";
4 | import { Badge } from "@/components/ui/badge";
5 | import { Button } from "@/components/ui/button";
6 | import { CertificateType } from "@/lib/types/certification-types";
7 | import { useQuery } from "@tanstack/react-query";
8 | import { MoveLeft, MoveRight } from "lucide-react";
9 | import Link from "next/link";
10 |
11 | const CertificationDetailContainer = ({
12 | certificationId,
13 | }: {
14 | certificationId: string;
15 | }) => {
16 | const { data } = useQuery({
17 | queryKey: ["certificate", certificationId],
18 | queryFn: () => getSingleCertificationAction(certificationId),
19 | });
20 |
21 | return (
22 | <>
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | {data?.organizationName}
33 |
34 |
35 |
36 | {data?.title}
37 |
38 |
39 | CredentionID ― {data?.credentialID}
40 |
41 |
42 |
What I learned:
43 |
44 | {data?.learned?.map((learn: any, index) => (
45 |
46 | {learn?.text}
47 |
48 | ))}
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 | See Certification
58 |
59 |
60 |
61 |
62 | <>
63 | Back
64 | >
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 | See Certification
73 |
74 |
75 |
76 |
77 | <>
78 | Back
79 | >
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 | >
88 | );
89 | };
90 | export default CertificationDetailContainer;
91 |
--------------------------------------------------------------------------------
/app/(public)/projects/_components/ProjectDetailContainer.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { getSingleProjectAction } from "@/actions/project.actions";
3 | import { ProjectMac } from "@/components/ProjectMac";
4 | import { Badge } from "@/components/ui/badge";
5 | import { Button } from "@/components/ui/button";
6 | import { Project } from "@/lib/types/project-types";
7 | import { useQuery } from "@tanstack/react-query";
8 | import { MoveLeft, MoveRight } from "lucide-react";
9 | import Link from "next/link";
10 |
11 | const ProjectDetailContainer = ({ projectId }: { projectId: string }) => {
12 | const { data } = useQuery({
13 | queryKey: ["project", projectId],
14 | queryFn: () => getSingleProjectAction(projectId),
15 | });
16 |
17 | return (
18 | <>
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | {data?.projectType}
28 |
29 |
30 | {data?.title}
31 |
32 |
33 | ― {data?.oneLiner}
34 |
35 |
36 |
37 |
{data?.description}
38 |
39 |
40 |
41 |
42 |
43 |
44 | {data?.liveURL.includes("github")
45 | ? "Source Code"
46 | : "See Live"}{" "}
47 |
48 |
49 |
50 |
51 |
52 |
Tech Stack:
53 | {data?.techStack?.map((tech: any) => (
54 |
55 | {tech?.text}
56 |
57 | ))}
58 |
59 |
60 |
Keywords:
61 | {data?.keywords?.map((key: any) => (
62 |
63 | #{key?.text}
64 |
65 | ))}
66 |
67 |
68 |
69 |
70 | See Live
71 |
72 |
73 |
74 |
75 |
76 | <>
77 | Back
78 | >
79 |
80 |
81 |
82 |
83 |
84 |
85 | >
86 | );
87 | };
88 | export default ProjectDetailContainer;
89 |
--------------------------------------------------------------------------------
/app/(public)/techstack/_components/TechStackPageContainer.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { getAllTechstacksAction } from "@/actions/techstack.actions";
4 | import TechstackCard from "@/components/TechstackCard";
5 | import { TechType } from "@/lib/types/techstack-types";
6 | import { useQuery } from "@tanstack/react-query";
7 |
8 | const TechStackPageContainer = () => {
9 | const { data, isPending } = useQuery({
10 | queryKey: ["techstacks"],
11 | queryFn: () => getAllTechstacksAction(),
12 | });
13 | const techstacks = data?.techstacks || [];
14 |
15 | const skills =
16 | data?.techstacks.filter(
17 | (item) => item?.techstackType === TechType.Skills
18 | ) || [];
19 | const devTools =
20 | data?.techstacks.filter(
21 | (item) => item?.techstackType === TechType.DevTools
22 | ) || [];
23 |
24 | const apps =
25 | data?.techstacks.filter((item) => item?.techstackType === TechType.Apps) ||
26 | [];
27 | const games =
28 | data?.techstacks.filter((item) => item?.techstackType === TechType.Games) ||
29 | [];
30 | const hardware =
31 | data?.techstacks.filter(
32 | (item) => item?.techstackType === TechType.Harware
33 | ) || [];
34 |
35 | if (isPending) return Please wait... ;
36 |
37 | if (techstacks.length < 0)
38 | return No techstack found! ;
39 |
40 | return (
41 |
42 | {skills?.length > 0 && (
43 | <>
44 |
45 | Skills
46 |
47 |
48 | {skills?.map((techstack) => (
49 |
50 | ))}
51 |
52 | >
53 | )}
54 | {devTools?.length > 0 && (
55 | <>
56 |
57 | Dev Tools
58 |
59 |
60 | {devTools?.map((techstack) => (
61 |
62 | ))}
63 |
64 | >
65 | )}
66 | {apps?.length > 0 && (
67 | <>
68 |
69 | Apps
70 |
71 |
72 | {apps?.map((techstack) => (
73 |
74 | ))}
75 |
76 | >
77 | )}
78 | {games?.length > 0 && (
79 | <>
80 |
81 | Games
82 |
83 |
84 | {games?.map((techstack) => (
85 |
86 | ))}
87 |
88 | >
89 | )}
90 | {hardware?.length > 0 && (
91 | <>
92 |
93 | Hardware
94 |
95 |
96 | {hardware?.map((techstack) => (
97 |
98 | ))}
99 |
100 | >
101 | )}
102 |
103 | );
104 | };
105 | export default TechStackPageContainer;
106 |
--------------------------------------------------------------------------------
/components/ui/tag.tsx:
--------------------------------------------------------------------------------
1 | import { X } from "lucide-react";
2 | import { Button } from "../ui/button";
3 | import { TagInputProps, type Tag as TagType } from "./tag-input";
4 | import { cn } from "@/lib/utils";
5 | import { cva } from "class-variance-authority";
6 |
7 | export const tagVariants = cva(
8 | "transition-all border inline-flex items-center text-sm pl-2 rounded-md",
9 | {
10 | variants: {
11 | variant: {
12 | default: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
13 | primary:
14 | "bg-primary border-primary text-primary-foreground hover:bg-primary/90",
15 | destructive:
16 | "bg-destructive border-destructive text-destructive-foreground hover:bg-destructive/90",
17 | },
18 | size: {
19 | sm: "text-xs h-7",
20 | md: "text-sm h-8",
21 | lg: "text-base h-9",
22 | xl: "text-lg h-10",
23 | },
24 | shape: {
25 | default: "rounded-sm",
26 | rounded: "rounded-lg",
27 | square: "rounded-none",
28 | pill: "rounded-full",
29 | },
30 | borderStyle: {
31 | default: "border-solid",
32 | none: "border-none",
33 | },
34 | textCase: {
35 | uppercase: "uppercase",
36 | lowercase: "lowercase",
37 | capitalize: "capitalize",
38 | },
39 | interaction: {
40 | clickable: "cursor-pointer hover:shadow-md",
41 | nonClickable: "cursor-default",
42 | },
43 | animation: {
44 | none: "",
45 | fadeIn: "animate-fadeIn",
46 | slideIn: "animate-slideIn",
47 | bounce: "animate-bounce",
48 | },
49 | textStyle: {
50 | normal: "font-normal",
51 | bold: "font-bold",
52 | italic: "italic",
53 | underline: "underline",
54 | lineThrough: "line-through",
55 | },
56 | },
57 | defaultVariants: {
58 | variant: "default",
59 | size: "md",
60 | shape: "default",
61 | borderStyle: "default",
62 | textCase: "capitalize",
63 | interaction: "nonClickable",
64 | animation: "fadeIn",
65 | textStyle: "normal",
66 | },
67 | }
68 | );
69 |
70 | export type TagProps = {
71 | tagObj: TagType;
72 | variant: TagInputProps["variant"];
73 | size: TagInputProps["size"];
74 | shape: TagInputProps["shape"];
75 | borderStyle: TagInputProps["borderStyle"];
76 | textCase: TagInputProps["textCase"];
77 | interaction: TagInputProps["interaction"];
78 | animation: TagInputProps["animation"];
79 | textStyle: TagInputProps["textStyle"];
80 | onRemoveTag: (id: string) => void;
81 | } & Pick;
82 |
83 | export const Tag: React.FC = ({
84 | tagObj,
85 | direction,
86 | draggable,
87 | onTagClick,
88 | onRemoveTag,
89 | variant,
90 | size,
91 | shape,
92 | borderStyle,
93 | textCase,
94 | interaction,
95 | animation,
96 | textStyle,
97 | }) => {
98 | return (
99 | onTagClick?.(tagObj)}>
119 | {tagObj.text}
120 | {
124 | e.stopPropagation(); // Prevent event from bubbling up to the tag span
125 | onRemoveTag(tagObj.id);
126 | }}
127 | className={cn("py-1 px-3 h-full hover:bg-transparent")}>
128 |
129 |
130 |
131 | );
132 | };
133 |
--------------------------------------------------------------------------------
/app/(admin)/admin-dashboard/manage-experience/_forms/AddExperienceForm.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { CldUploadButton } from "next-cloudinary";
3 |
4 | //tags
5 | import { Form } from "@/components/ui/form";
6 |
7 | import { Tag } from "@/components/ui/tag-input";
8 | import { Button } from "@/components/ui/button";
9 | import { useForm } from "react-hook-form";
10 | import { zodResolver } from "@hookform/resolvers/zod";
11 | //end tags
12 |
13 | import {
14 | CustomFormField,
15 | CustomFormFieldFile,
16 | CustomTagField,
17 | } from "@/components/FormComponents";
18 |
19 | import { useMutation, useQueryClient } from "@tanstack/react-query";
20 | import { useToast } from "@/components/ui/use-toast";
21 | import { useRouter } from "next/navigation";
22 | import { useState } from "react";
23 | import {
24 | CreateAndEditExperienceType,
25 | ExperienceType,
26 | createAndEditExperienceSchema,
27 | } from "@/lib/types/experience-types";
28 |
29 | import { createExperienceAction } from "@/actions/experience.actions";
30 | import { DatePicker } from "@/components/DatePicker";
31 |
32 | function AddExperienceForm() {
33 | const queryClient = useQueryClient();
34 | const { toast } = useToast();
35 | const router = useRouter();
36 |
37 | const [whatILearned, setWhatILearned] = useState([]);
38 |
39 | const defaultDate: Date = new Date(Date.now()) || "2000-05-13";
40 |
41 | const form = useForm({
42 | resolver: zodResolver(createAndEditExperienceSchema),
43 | defaultValues: {
44 | positionName: "",
45 | companyName: "",
46 | companyLocation: "",
47 | startDate: defaultDate,
48 | endDate: defaultDate,
49 | learned: [],
50 | },
51 | });
52 |
53 | const { setValue } = form;
54 |
55 | const { mutate, isPending } = useMutation({
56 | mutationFn: (values: CreateAndEditExperienceType) =>
57 | createExperienceAction(values),
58 | onSuccess: (data) => {
59 | if (!data) {
60 | toast({ description: "there was an error" });
61 | return;
62 | }
63 | router.push("/admin-dashboard/manage-experience");
64 | toast({ description: "Experience added successfully!" });
65 | queryClient.invalidateQueries({ queryKey: ["experience"] });
66 | queryClient.invalidateQueries({ queryKey: ["stats"] });
67 | return null;
68 | },
69 | });
70 |
71 | function onSubmit(values: CreateAndEditExperienceType) {
72 | mutate(values);
73 | }
74 | return (
75 |
121 |
122 | );
123 | }
124 | export default AddExperienceForm;
125 |
--------------------------------------------------------------------------------
/app/(admin)/admin-dashboard/manage-techstack/_forms/AddTechstackForm.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { CldUploadButton } from "next-cloudinary";
3 |
4 | //tags
5 | import { Form } from "@/components/ui/form";
6 | import { Button } from "@/components/ui/button";
7 | import { useForm } from "react-hook-form";
8 | import { zodResolver } from "@hookform/resolvers/zod";
9 | //end tags
10 |
11 | import {
12 | CustomFormField,
13 | CustomFormFieldFile,
14 | CustomFormSelect,
15 | } from "@/components/FormComponents";
16 |
17 | import { useMutation, useQueryClient } from "@tanstack/react-query";
18 | import { useToast } from "@/components/ui/use-toast";
19 | import { useRouter } from "next/navigation";
20 | import { useState } from "react";
21 | import {
22 | CreateAndEditTechstackType,
23 | TechType,
24 | createAndEditTechstackType,
25 | } from "@/lib/types/techstack-types";
26 | import { createTechstackAction } from "@/actions/techstack.actions";
27 |
28 | function AddTechstackForm() {
29 | const queryClient = useQueryClient();
30 | const { toast } = useToast();
31 | const router = useRouter();
32 |
33 | const [logoUrl, setLogoUrl] = useState("");
34 |
35 | const form = useForm({
36 | resolver: zodResolver(createAndEditTechstackType),
37 | defaultValues: {
38 | title: "",
39 | category: "",
40 | techstackType: TechType.Skills,
41 | imageUrl: "",
42 | url: "",
43 | },
44 | });
45 |
46 | const { mutate, isPending } = useMutation({
47 | mutationFn: (values: CreateAndEditTechstackType) =>
48 | createTechstackAction(values),
49 | onSuccess: (data) => {
50 | if (!data) {
51 | toast({ description: "there was an error" });
52 | return;
53 | }
54 | router.push("/admin-dashboard/manage-techstack");
55 | toast({ description: "Techstack added successfully!" });
56 | queryClient.invalidateQueries({ queryKey: ["techstacks"] });
57 | queryClient.invalidateQueries({ queryKey: ["stats"] });
58 | return null;
59 | },
60 | });
61 |
62 | function onSubmit(values: CreateAndEditTechstackType) {
63 | const data: CreateAndEditTechstackType = {
64 | ...values,
65 | imageUrl: logoUrl,
66 | };
67 | mutate(data);
68 | }
69 |
70 | return (
71 |
120 |
121 | );
122 | }
123 | export default AddTechstackForm;
124 |
--------------------------------------------------------------------------------
/actions/project.actions.ts:
--------------------------------------------------------------------------------
1 | 'use server';
2 |
3 | import prisma from "@/db";
4 | import {
5 | CreateAndEditProjectType,
6 | Project,
7 | createAndEditProjectSchema,
8 | } from "@/lib/types/project-types";
9 |
10 | import { auth } from '@clerk/nextjs';
11 | import { redirect } from 'next/navigation';
12 |
13 |
14 | function authenticateAndRedirect(): string {
15 | const { userId } = auth();
16 | if (!userId) redirect('/');
17 | return userId;
18 | }
19 |
20 |
21 | export async function createProjectAction(values: CreateAndEditProjectType): Promise {
22 | try {
23 | createAndEditProjectSchema.parse(values);
24 |
25 | const project: Project = await prisma.project.create({
26 | data: {
27 | ...values
28 | }
29 | });
30 |
31 | return project;
32 | } catch (error) {
33 | console.log(error);
34 | return null;
35 | }
36 | }
37 |
38 |
39 | export async function getAllProjectsAction(): Promise<{
40 | projects: Project[]
41 | }> {
42 | try {
43 | const projects: Project[] = await prisma.project.findMany({})
44 | return { projects };
45 | } catch (error) {
46 | console.log(error);
47 | return { projects: [] };
48 | }
49 | }
50 |
51 | function shuffleProjects(projects: Project[]): Project[] {
52 | // Deep copy the original array to avoid mutating the original array
53 | const shuffledProjects = [...projects];
54 |
55 | // Fisher-Yates shuffle algorithm
56 | for (let i = shuffledProjects.length - 1; i > 0; i--) {
57 | const j = Math.floor(Math.random() * (i + 1));
58 | [shuffledProjects[i], shuffledProjects[j]] = [shuffledProjects[j], shuffledProjects[i]];
59 | }
60 |
61 | return shuffledProjects;
62 | }
63 |
64 | export async function getRandomProjectsAction(): Promise<{ title: string; link: string; thumbnail: string }[]> {
65 | try {
66 | const randomProjects = await prisma.project.findMany({
67 | take: 15,
68 | orderBy: {
69 | id: 'asc'
70 | }
71 | });
72 |
73 | const shuffledProjects = shuffleProjects(randomProjects);
74 |
75 |
76 | // Map the random projects to the desired format
77 | const projects = shuffledProjects.map(project => ({
78 | title: project.title,
79 | link: project.liveURL,
80 | thumbnail: project.screenshot
81 | }));
82 |
83 | return projects;
84 | } catch (error) {
85 | console.error('Error fetching and mapping random projects:', error);
86 | throw error;
87 | }
88 | }
89 |
90 |
91 | export async function getSingleProjectAction(id: string): Promise {
92 | let project: Project | null = null;
93 |
94 | try {
95 | project = await prisma.project.findUnique({
96 | where: {
97 | id,
98 | },
99 | });
100 | } catch (error) {
101 | project = null;
102 | }
103 | if (!project) {
104 | redirect('/admin-dashboard/manage-projects');
105 | }
106 | return project;
107 | }
108 |
109 | export async function deleteProjectAction(id: string): Promise {
110 | const userId = authenticateAndRedirect();
111 |
112 | try {
113 | const project: Project = await prisma.project.delete({
114 | where: {
115 | id,
116 | },
117 | });
118 | return project;
119 | } catch (error) {
120 | return null;
121 | }
122 | }
123 |
124 | export async function updateProjectAction(
125 | id: string,
126 | values: CreateAndEditProjectType
127 | ): Promise {
128 | const userId = authenticateAndRedirect();
129 |
130 | try {
131 | const project: Project = await prisma.project.update({
132 | where: {
133 | id,
134 | },
135 | data: {
136 | ...values,
137 | },
138 | });
139 | return project;
140 | } catch (error) {
141 | return null;
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DialogPrimitive from "@radix-ui/react-dialog"
5 | import { Cross2Icon } from "@radix-ui/react-icons"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Dialog = DialogPrimitive.Root
10 |
11 | const DialogTrigger = DialogPrimitive.Trigger
12 |
13 | const DialogPortal = DialogPrimitive.Portal
14 |
15 | const DialogClose = DialogPrimitive.Close
16 |
17 | const DialogOverlay = React.forwardRef<
18 | React.ElementRef,
19 | React.ComponentPropsWithoutRef
20 | >(({ className, ...props }, ref) => (
21 |
29 | ))
30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
31 |
32 | const DialogContent = React.forwardRef<
33 | React.ElementRef,
34 | React.ComponentPropsWithoutRef
35 | >(({ className, children, ...props }, ref) => (
36 |
37 |
38 |
46 | {children}
47 |
48 |
49 | Close
50 |
51 |
52 |
53 | ))
54 | DialogContent.displayName = DialogPrimitive.Content.displayName
55 |
56 | const DialogHeader = ({
57 | className,
58 | ...props
59 | }: React.HTMLAttributes) => (
60 |
67 | )
68 | DialogHeader.displayName = "DialogHeader"
69 |
70 | const DialogFooter = ({
71 | className,
72 | ...props
73 | }: React.HTMLAttributes) => (
74 |
81 | )
82 | DialogFooter.displayName = "DialogFooter"
83 |
84 | const DialogTitle = React.forwardRef<
85 | React.ElementRef,
86 | React.ComponentPropsWithoutRef
87 | >(({ className, ...props }, ref) => (
88 |
96 | ))
97 | DialogTitle.displayName = DialogPrimitive.Title.displayName
98 |
99 | const DialogDescription = React.forwardRef<
100 | React.ElementRef,
101 | React.ComponentPropsWithoutRef
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | DialogDescription.displayName = DialogPrimitive.Description.displayName
110 |
111 | export {
112 | Dialog,
113 | DialogPortal,
114 | DialogOverlay,
115 | DialogTrigger,
116 | DialogClose,
117 | DialogContent,
118 | DialogHeader,
119 | DialogFooter,
120 | DialogTitle,
121 | DialogDescription,
122 | }
123 |
--------------------------------------------------------------------------------
/app/(admin)/admin-dashboard/manage-experience/_forms/EditExperienceForm.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import {
4 | getSingleExperienceAction,
5 | updateExperienceAction,
6 | } from "@/actions/experience.actions";
7 |
8 | import { DatePicker } from "@/components/DatePicker";
9 |
10 | import { CustomFormField, CustomTagField } from "@/components/FormComponents";
11 |
12 | import { Button } from "@/components/ui/button";
13 |
14 | import { Form } from "@/components/ui/form";
15 |
16 | import { Tag } from "@/components/ui/tag-input";
17 |
18 | import { useToast } from "@/components/ui/use-toast";
19 |
20 | import {
21 | CreateAndEditExperienceType,
22 | createAndEditExperienceSchema,
23 | } from "@/lib/types/experience-types";
24 |
25 | import { zodResolver } from "@hookform/resolvers/zod";
26 | import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
27 | import { useRouter } from "next/navigation";
28 | import { useState } from "react";
29 | import { useForm } from "react-hook-form";
30 |
31 | function EditExperienceForm({ experienceId }: { experienceId: string }) {
32 | const queryClient = useQueryClient();
33 | const { toast } = useToast();
34 | const router = useRouter();
35 |
36 | const { data } = useQuery({
37 | queryKey: ["experience", experienceId],
38 | queryFn: () => getSingleExperienceAction(experienceId),
39 | });
40 |
41 | function onSubmit(values: CreateAndEditExperienceType) {
42 | mutate(values);
43 | }
44 | const [whatILearned, setWhatILearned] = useState(
45 | data?.learned as Tag[]
46 | );
47 |
48 | const form = useForm({
49 | resolver: zodResolver(createAndEditExperienceSchema),
50 | defaultValues: {
51 | positionName: data?.positionName,
52 | companyName: data?.companyName,
53 | companyLocation: data?.companyLocation,
54 | startDate: data?.startDate,
55 | endDate: data?.endDate,
56 | learned: data?.learned as Tag[],
57 | },
58 | });
59 | const { setValue } = form;
60 |
61 | const { mutate, isPending } = useMutation({
62 | mutationFn: (values: CreateAndEditExperienceType) =>
63 | updateExperienceAction(experienceId, values),
64 | onSuccess: (data) => {
65 | if (!data) {
66 | toast({ description: "There was an error" });
67 | return;
68 | }
69 | router.push("/admin-dashboard/manage-experience");
70 | toast({ description: "Experience updated successfully!" });
71 | queryClient.invalidateQueries({
72 | queryKey: ["experience"],
73 | });
74 | queryClient.invalidateQueries({
75 | queryKey: ["stats"],
76 | });
77 | queryClient.invalidateQueries({
78 | queryKey: ["experience", experienceId],
79 | });
80 | return null;
81 | },
82 | });
83 |
84 | return (
85 |
131 |
132 | );
133 | }
134 |
135 | export default EditExperienceForm;
136 |
--------------------------------------------------------------------------------
/components/FormComponents.tsx:
--------------------------------------------------------------------------------
1 | import { Control } from "react-hook-form";
2 | import {
3 | Select,
4 | SelectContent,
5 | SelectItem,
6 | SelectTrigger,
7 | SelectValue,
8 | } from "@/components/ui/select";
9 | import {
10 | FormControl,
11 | FormField,
12 | FormItem,
13 | FormLabel,
14 | FormMessage,
15 | } from "@/components/ui/form";
16 | import { Input } from "./ui/input";
17 | import { Textarea } from "./ui/textarea";
18 | import { Tag, TagInput } from "./ui/tag-input";
19 | import { SetStateAction } from "react";
20 |
21 | type CustomFormFieldProps = {
22 | name: string;
23 | control: Control;
24 | title?: string;
25 | };
26 |
27 | export function CustomFormField({
28 | name,
29 | control,
30 | title,
31 | }: CustomFormFieldProps) {
32 | return (
33 | (
37 |
38 | {title || name}
39 |
40 |
41 |
42 |
43 |
44 | )}
45 | />
46 | );
47 | }
48 |
49 | export function CustomFormFieldFile({
50 | name,
51 | title,
52 | control,
53 | value,
54 | }: {
55 | name: string;
56 | control: Control;
57 | value: string;
58 | title?: string;
59 | }) {
60 | return (
61 | (
66 |
67 | {title || name}
68 |
69 |
70 |
71 |
72 |
73 | )}
74 | />
75 | );
76 | }
77 |
78 | export function CustomFormTextArea({
79 | name,
80 | control,
81 | title,
82 | }: CustomFormFieldProps) {
83 | return (
84 | (
88 |
89 | {title || name}
90 |
91 |
92 |
93 |
94 |
95 | )}
96 | />
97 | );
98 | }
99 |
100 | type CustomFormSelectProps = {
101 | name: string;
102 | control: Control;
103 | items: string[];
104 | labelText?: string;
105 | };
106 |
107 | export function CustomFormSelect({
108 | name,
109 | control,
110 | items,
111 | labelText,
112 | }: CustomFormSelectProps) {
113 | return (
114 | {
118 | return (
119 |
120 | {labelText || name}
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 | {items.map((item) => {
129 | return (
130 |
131 | {item}
132 |
133 | );
134 | })}
135 |
136 |
137 |
138 |
139 | );
140 | }}
141 | />
142 | );
143 | }
144 |
145 | export function CustomTagField({
146 | name,
147 | title,
148 | control,
149 | tagsList,
150 | setValue,
151 | setTagsList,
152 | }: {
153 | name: string;
154 | control: Control;
155 | title?: string;
156 | tagsList: Tag[];
157 | setTagsList: any;
158 | setValue: any;
159 | }) {
160 | return (
161 | (
165 |
166 | {title || name}
167 |
168 | {
172 | setTagsList(newTags);
173 | setValue(name, newTags as [Tag, ...Tag[]]);
174 | }}
175 | />
176 |
177 |
178 |
179 | )}
180 | />
181 | );
182 | }
183 |
--------------------------------------------------------------------------------
/app/(admin)/admin-dashboard/manage-techstack/_forms/EditTechstackForm.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import {
4 | getSingleTechstackAction,
5 | updateTechstackAction,
6 | } from "@/actions/techstack.actions";
7 |
8 | import { DatePicker } from "@/components/DatePicker";
9 |
10 | import {
11 | CustomFormField,
12 | CustomFormFieldFile,
13 | CustomFormSelect,
14 | CustomTagField,
15 | } from "@/components/FormComponents";
16 |
17 | import { Button } from "@/components/ui/button";
18 |
19 | import { Form } from "@/components/ui/form";
20 |
21 | import { Tag } from "@/components/ui/tag-input";
22 |
23 | import { useToast } from "@/components/ui/use-toast";
24 | import {
25 | CreateAndEditTechstackType,
26 | TechType,
27 | createAndEditTechstackType,
28 | } from "@/lib/types/techstack-types";
29 |
30 | import { zodResolver } from "@hookform/resolvers/zod";
31 | import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
32 | import { CldUploadButton } from "next-cloudinary";
33 | import { useRouter } from "next/navigation";
34 | import { useState } from "react";
35 | import { useForm } from "react-hook-form";
36 |
37 | function EditTechstackForm({ techstackId }: { techstackId: string }) {
38 | const queryClient = useQueryClient();
39 | const { toast } = useToast();
40 | const router = useRouter();
41 |
42 | const { data } = useQuery({
43 | queryKey: ["techstack", techstackId],
44 | queryFn: () => getSingleTechstackAction(techstackId),
45 | });
46 |
47 | function onSubmit(values: CreateAndEditTechstackType) {
48 | const data: CreateAndEditTechstackType = {
49 | ...values,
50 | imageUrl: logoUrl,
51 | };
52 |
53 | mutate(data);
54 | }
55 |
56 | const [logoUrl, setLogoUrl] = useState(data?.imageUrl || "");
57 |
58 | const form = useForm({
59 | resolver: zodResolver(createAndEditTechstackType),
60 | defaultValues: {
61 | imageUrl: logoUrl,
62 | title: data?.title,
63 | category: data?.category,
64 | url: data?.url,
65 | techstackType: data?.techstackType as TechType,
66 | },
67 | });
68 | const { setValue } = form;
69 |
70 | const { mutate, isPending } = useMutation({
71 | mutationFn: (values: CreateAndEditTechstackType) =>
72 | updateTechstackAction(techstackId, values),
73 | onSuccess: (data) => {
74 | if (!data) {
75 | toast({ description: "There was an error" });
76 | return;
77 | }
78 | router.push("/admin-dashboard/manage-techstack");
79 | toast({ description: "Certificate updated successfully!" });
80 | queryClient.invalidateQueries({
81 | queryKey: ["techstacks"],
82 | });
83 | queryClient.invalidateQueries({
84 | queryKey: ["stats"],
85 | });
86 | queryClient.invalidateQueries({
87 | queryKey: ["techstack", techstackId],
88 | });
89 | return null;
90 | },
91 | });
92 |
93 | return (
94 |
143 |
144 | );
145 | }
146 |
147 | export default EditTechstackForm;
148 |
--------------------------------------------------------------------------------
/components/ui/use-toast.ts:
--------------------------------------------------------------------------------
1 | // Inspired by react-hot-toast library
2 | import * as React from "react"
3 |
4 | import type {
5 | ToastActionElement,
6 | ToastProps,
7 | } from "@/components/ui/toast"
8 |
9 | const TOAST_LIMIT = 1
10 | const TOAST_REMOVE_DELAY = 1000000
11 |
12 | type ToasterToast = ToastProps & {
13 | id: string
14 | title?: React.ReactNode
15 | description?: React.ReactNode
16 | action?: ToastActionElement
17 | }
18 |
19 | const actionTypes = {
20 | ADD_TOAST: "ADD_TOAST",
21 | UPDATE_TOAST: "UPDATE_TOAST",
22 | DISMISS_TOAST: "DISMISS_TOAST",
23 | REMOVE_TOAST: "REMOVE_TOAST",
24 | } as const
25 |
26 | let count = 0
27 |
28 | function genId() {
29 | count = (count + 1) % Number.MAX_SAFE_INTEGER
30 | return count.toString()
31 | }
32 |
33 | type ActionType = typeof actionTypes
34 |
35 | type Action =
36 | | {
37 | type: ActionType["ADD_TOAST"]
38 | toast: ToasterToast
39 | }
40 | | {
41 | type: ActionType["UPDATE_TOAST"]
42 | toast: Partial
43 | }
44 | | {
45 | type: ActionType["DISMISS_TOAST"]
46 | toastId?: ToasterToast["id"]
47 | }
48 | | {
49 | type: ActionType["REMOVE_TOAST"]
50 | toastId?: ToasterToast["id"]
51 | }
52 |
53 | interface State {
54 | toasts: ToasterToast[]
55 | }
56 |
57 | const toastTimeouts = new Map>()
58 |
59 | const addToRemoveQueue = (toastId: string) => {
60 | if (toastTimeouts.has(toastId)) {
61 | return
62 | }
63 |
64 | const timeout = setTimeout(() => {
65 | toastTimeouts.delete(toastId)
66 | dispatch({
67 | type: "REMOVE_TOAST",
68 | toastId: toastId,
69 | })
70 | }, TOAST_REMOVE_DELAY)
71 |
72 | toastTimeouts.set(toastId, timeout)
73 | }
74 |
75 | export const reducer = (state: State, action: Action): State => {
76 | switch (action.type) {
77 | case "ADD_TOAST":
78 | return {
79 | ...state,
80 | toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
81 | }
82 |
83 | case "UPDATE_TOAST":
84 | return {
85 | ...state,
86 | toasts: state.toasts.map((t) =>
87 | t.id === action.toast.id ? { ...t, ...action.toast } : t
88 | ),
89 | }
90 |
91 | case "DISMISS_TOAST": {
92 | const { toastId } = action
93 |
94 | // ! Side effects ! - This could be extracted into a dismissToast() action,
95 | // but I'll keep it here for simplicity
96 | if (toastId) {
97 | addToRemoveQueue(toastId)
98 | } else {
99 | state.toasts.forEach((toast) => {
100 | addToRemoveQueue(toast.id)
101 | })
102 | }
103 |
104 | return {
105 | ...state,
106 | toasts: state.toasts.map((t) =>
107 | t.id === toastId || toastId === undefined
108 | ? {
109 | ...t,
110 | open: false,
111 | }
112 | : t
113 | ),
114 | }
115 | }
116 | case "REMOVE_TOAST":
117 | if (action.toastId === undefined) {
118 | return {
119 | ...state,
120 | toasts: [],
121 | }
122 | }
123 | return {
124 | ...state,
125 | toasts: state.toasts.filter((t) => t.id !== action.toastId),
126 | }
127 | }
128 | }
129 |
130 | const listeners: Array<(state: State) => void> = []
131 |
132 | let memoryState: State = { toasts: [] }
133 |
134 | function dispatch(action: Action) {
135 | memoryState = reducer(memoryState, action)
136 | listeners.forEach((listener) => {
137 | listener(memoryState)
138 | })
139 | }
140 |
141 | type Toast = Omit
142 |
143 | function toast({ ...props }: Toast) {
144 | const id = genId()
145 |
146 | const update = (props: ToasterToast) =>
147 | dispatch({
148 | type: "UPDATE_TOAST",
149 | toast: { ...props, id },
150 | })
151 | const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
152 |
153 | dispatch({
154 | type: "ADD_TOAST",
155 | toast: {
156 | ...props,
157 | id,
158 | open: true,
159 | onOpenChange: (open) => {
160 | if (!open) dismiss()
161 | },
162 | },
163 | })
164 |
165 | return {
166 | id: id,
167 | dismiss,
168 | update,
169 | }
170 | }
171 |
172 | function useToast() {
173 | const [state, setState] = React.useState(memoryState)
174 |
175 | React.useEffect(() => {
176 | listeners.push(setState)
177 | return () => {
178 | const index = listeners.indexOf(setState)
179 | if (index > -1) {
180 | listeners.splice(index, 1)
181 | }
182 | }
183 | }, [state])
184 |
185 | return {
186 | ...state,
187 | toast,
188 | dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
189 | }
190 | }
191 |
192 | export { useToast, toast }
193 |
--------------------------------------------------------------------------------
/components/ui/sheet.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SheetPrimitive from "@radix-ui/react-dialog"
5 | import { Cross2Icon } from "@radix-ui/react-icons"
6 | import { cva, type VariantProps } from "class-variance-authority"
7 |
8 | import { cn } from "@/lib/utils"
9 |
10 | const Sheet = SheetPrimitive.Root
11 |
12 | const SheetTrigger = SheetPrimitive.Trigger
13 |
14 | const SheetClose = SheetPrimitive.Close
15 |
16 | const SheetPortal = SheetPrimitive.Portal
17 |
18 | const SheetOverlay = React.forwardRef<
19 | React.ElementRef,
20 | React.ComponentPropsWithoutRef
21 | >(({ className, ...props }, ref) => (
22 |
30 | ))
31 | SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
32 |
33 | const sheetVariants = cva(
34 | "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
35 | {
36 | variants: {
37 | side: {
38 | top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
39 | bottom:
40 | "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
41 | left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
42 | right:
43 | "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
44 | },
45 | },
46 | defaultVariants: {
47 | side: "right",
48 | },
49 | }
50 | )
51 |
52 | interface SheetContentProps
53 | extends React.ComponentPropsWithoutRef,
54 | VariantProps {}
55 |
56 | const SheetContent = React.forwardRef<
57 | React.ElementRef,
58 | SheetContentProps
59 | >(({ side = "right", className, children, ...props }, ref) => (
60 |
61 |
62 |
67 | {children}
68 |
69 |
70 | Close
71 |
72 |
73 |
74 | ))
75 | SheetContent.displayName = SheetPrimitive.Content.displayName
76 |
77 | const SheetHeader = ({
78 | className,
79 | ...props
80 | }: React.HTMLAttributes) => (
81 |
88 | )
89 | SheetHeader.displayName = "SheetHeader"
90 |
91 | const SheetFooter = ({
92 | className,
93 | ...props
94 | }: React.HTMLAttributes) => (
95 |
102 | )
103 | SheetFooter.displayName = "SheetFooter"
104 |
105 | const SheetTitle = React.forwardRef<
106 | React.ElementRef,
107 | React.ComponentPropsWithoutRef
108 | >(({ className, ...props }, ref) => (
109 |
114 | ))
115 | SheetTitle.displayName = SheetPrimitive.Title.displayName
116 |
117 | const SheetDescription = React.forwardRef<
118 | React.ElementRef,
119 | React.ComponentPropsWithoutRef
120 | >(({ className, ...props }, ref) => (
121 |
126 | ))
127 | SheetDescription.displayName = SheetPrimitive.Description.displayName
128 |
129 | export {
130 | Sheet,
131 | SheetPortal,
132 | SheetOverlay,
133 | SheetTrigger,
134 | SheetClose,
135 | SheetContent,
136 | SheetHeader,
137 | SheetFooter,
138 | SheetTitle,
139 | SheetDescription,
140 | }
141 |
--------------------------------------------------------------------------------