diff --git a/apps/client/src/features/cv-templates/mutations/create-cv.graphql b/apps/client/src/features/cv-templates/mutations/create-cv.graphql new file mode 100644 index 0000000..d0d0e5d --- /dev/null +++ b/apps/client/src/features/cv-templates/mutations/create-cv.graphql @@ -0,0 +1,13 @@ +mutation CreateCV($input: CreateCVInput!) { + createCV(input: $input) { + id + title + template { + id + name + description + } + createdAt + updatedAt + } +} diff --git a/apps/client/src/features/cv-templates/mutations/delete-cv.graphql b/apps/client/src/features/cv-templates/mutations/delete-cv.graphql new file mode 100644 index 0000000..d27d422 --- /dev/null +++ b/apps/client/src/features/cv-templates/mutations/delete-cv.graphql @@ -0,0 +1,3 @@ +mutation DeleteCV($id: String!) { + deleteCV(id: $id) +} diff --git a/apps/client/src/features/cv-templates/mutations/update-cv.graphql b/apps/client/src/features/cv-templates/mutations/update-cv.graphql new file mode 100644 index 0000000..8d7a5cc --- /dev/null +++ b/apps/client/src/features/cv-templates/mutations/update-cv.graphql @@ -0,0 +1,13 @@ +mutation UpdateCV($id: String!, $input: UpdateCVInput!) { + updateCV(id: $id, input: $input) { + id + title + template { + id + name + description + } + createdAt + updatedAt + } +} diff --git a/apps/client/src/features/cv-templates/queries/cv-template.graphql b/apps/client/src/features/cv-templates/queries/cv-template.graphql new file mode 100644 index 0000000..ebc849c --- /dev/null +++ b/apps/client/src/features/cv-templates/queries/cv-template.graphql @@ -0,0 +1,9 @@ +query CVTemplate($id: String!) { + cvTemplate(id: $id) { + id + name + description + createdAt + updatedAt + } +} diff --git a/apps/client/src/features/cv-templates/queries/cv-templates.graphql b/apps/client/src/features/cv-templates/queries/cv-templates.graphql new file mode 100644 index 0000000..31c3c90 --- /dev/null +++ b/apps/client/src/features/cv-templates/queries/cv-templates.graphql @@ -0,0 +1,21 @@ +query CVTemplates($first: Int, $after: String) { + cvTemplates(first: $first, after: $after) { + edges { + node { + id + name + description + createdAt + updatedAt + } + cursor + } + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + totalCount + } +} diff --git a/apps/client/src/features/cv-templates/queries/cv.graphql b/apps/client/src/features/cv-templates/queries/cv.graphql new file mode 100644 index 0000000..2699db7 --- /dev/null +++ b/apps/client/src/features/cv-templates/queries/cv.graphql @@ -0,0 +1,14 @@ +query CV($id: String!) { + cv(id: $id) { + id + title + introduction + createdAt + updatedAt + template { + id + name + description + } + } +} diff --git a/apps/client/src/features/cv-templates/queries/my-cvs.graphql b/apps/client/src/features/cv-templates/queries/my-cvs.graphql new file mode 100644 index 0000000..4e47b2a --- /dev/null +++ b/apps/client/src/features/cv-templates/queries/my-cvs.graphql @@ -0,0 +1,27 @@ +query MyCVs($first: Int, $after: String) { + me { + cvs(first: $first, after: $after) { + edges { + node { + id + title + template { + id + name + description + } + createdAt + updatedAt + } + cursor + } + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + totalCount + } + } +} diff --git a/apps/client/src/pages/CVViewPage.tsx b/apps/client/src/pages/CVViewPage.tsx new file mode 100644 index 0000000..1c1e543 --- /dev/null +++ b/apps/client/src/pages/CVViewPage.tsx @@ -0,0 +1,222 @@ +import { Button, FormattedDateRange } from "@cv/ui"; +import { Link, useParams } from "react-router-dom"; +import { + useCvQuery, + useMeEducationQuery, + useMeJobExperienceQuery, +} from "@/generated/graphql"; + +export const CVViewPage = () => { + const { id } = useParams<{ id: string }>(); + const { data, isLoading } = useCvQuery({ id: id || "" }, { enabled: !!id }); + const { data: jobExperienceData, isLoading: loadingJobs } = + useMeJobExperienceQuery(); + const { data: educationData, isLoading: loadingEducation } = + useMeEducationQuery(); + + if (isLoading) { + return ( +
+
Loading CV...
+
+ ); + } + + if (!data?.cv) { + return ( +
+

CV Not Found

+ + + +
+ ); + } + + const { cv } = data; + const jobExperiences = + jobExperienceData?.me?.experience?.edges?.map((edge) => edge.node) || []; + const educations = + educationData?.me?.educationHistory?.edges?.map((edge) => edge.node) || []; + + return ( +
+
+
+
+

{cv.title}

+

+ Template: {cv.template.name} +

+
+
+ + + + + + +
+
+
+ +
+
+ {/* CV Header */} +
+

+ {cv.title} +

+ {cv.introduction && ( +

+ {cv.introduction} +

+ )} +
+ + {/* Education Section */} + {loadingEducation ? ( +
+ Loading education history... +
+ ) : educations.length === 0 ? ( +
+ No education history recorded yet. Add your educational background + to see it here. +
+ ) : ( +
+

+ Education +

+ + {educations.map((education) => ( +
+
+
+

+ {education.degree} +

+

+ {education.institution?.name || "Unknown Institution"} +

+ {education.fieldOfStudy && ( +

+ {education.fieldOfStudy} +

+ )} +
+
+ +
+
+ + {education.description && ( +

+ {education.description} +

+ )} + + {education.skills && education.skills.length > 0 && ( +
+

+ Skills: +

+
+ {education.skills.map((skill) => ( + + {skill.name} + + ))} +
+
+ )} +
+ ))} +
+ )} + + {/* Job Experience Section */} + {loadingJobs ? ( +
+ Loading job experience... +
+ ) : jobExperiences.length === 0 ? ( +
+ No job experience recorded yet. Add your work history to see it + here. +
+ ) : ( +
+

+ Work Experience +

+ + {jobExperiences.map((experience) => ( +
+
+
+

+ {experience.role.name} +

+

+ {experience.company.name} +

+ {experience.level && ( +

+ {experience.level.name} +

+ )} +
+
+ +
+
+ + {experience.description && ( +

+ {experience.description} +

+ )} + + {experience.skills && experience.skills.length > 0 && ( +
+

+ Skills: +

+
+ {experience.skills.map((skill) => ( + + {skill.name} + + ))} +
+
+ )} +
+ ))} +
+ )} +
+
+
+ ); +}; diff --git a/apps/client/src/pages/CVsPage.tsx b/apps/client/src/pages/CVsPage.tsx new file mode 100644 index 0000000..2af6b64 --- /dev/null +++ b/apps/client/src/pages/CVsPage.tsx @@ -0,0 +1,137 @@ +import { + Button, + DeleteIcon, + EditIcon, + IconButton, + PageHeader, + Placeholder, +} from "@cv/ui"; +import { useQueryClient } from "@tanstack/react-query"; +import { Link } from "react-router-dom"; +import { useConfirmationModal } from "@/contexts/ConfirmationModalContext"; +import { useToast } from "@/contexts/ToastContext"; +import { useDeleteCvMutation, useMyCVsQuery } from "@/generated/graphql"; + +export const CVsPage = () => { + const { data, isLoading } = useMyCVsQuery({}); + const { mutateAsync: deleteCV } = useDeleteCvMutation(); + const queryClient = useQueryClient(); + const { showSuccess, showError } = useToast(); + const { showConfirmation } = useConfirmationModal(); + + const handleDelete = async (id: string, title: string) => { + const _confirmed = await showConfirmation({ + title: "Delete CV", + message: `Are you sure you want to delete "${title}"? This action cannot be undone.`, + confirmText: "Delete", + cancelText: "Cancel", + variant: "danger", + onConfirm: async () => { + try { + await deleteCV({ id }); + + // Invalidate and refetch CV queries + await queryClient.invalidateQueries({ + queryKey: ["MyCVs"], + }); + + showSuccess("CV Deleted", "Your CV has been successfully deleted."); + } catch (error) { + console.error("Error deleting CV:", error); + showError( + "Failed to Delete CV", + "There was an error deleting the CV. Please try again.", + ); + } + }, + }); + }; + + if (isLoading) { + return ( +
+ + +
+ ); + } + + const cvs = data?.me?.cvs?.edges?.map((edge) => edge.node) || []; + + return ( +
+ + + + } + /> + + {cvs.length === 0 ? ( + + + + + + ) : ( +
+ {cvs.map((cv) => ( +
+
+
+

+ {cv.title} +

+

+ Template: {cv.template.name} +

+
+
+ + } + label="Edit" + variant="primary" + showColorOnHover={true} + /> + + } + label="Delete" + variant="destructive" + showColorOnHover={true} + onClick={() => handleDelete(cv.id, cv.title)} + /> +
+
+ +
+ Updated: {new Date(cv.updatedAt).toLocaleDateString()} +
+ +
+ + + +
+
+ ))} +
+ )} +
+ ); +};