because I got bored of customising my CV for every job
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(client): wire edit flows for job experience and education

+365 -73
+62 -32
apps/client/src/features/education/components/EducationForm.tsx
··· 10 10 import { useId, useState } from "react"; 11 11 import { SelectedSkillsDisplay } from "@/features/job-experience/components/SelectedSkillsDisplay"; 12 12 import { SkillsSelect } from "@/features/job-experience/components/SkillsSelect"; 13 - import { useCreateEducationMutation } from "@/generated/graphql"; 13 + import { 14 + useCreateEducationMutation, 15 + useUpdateEducationMutation, 16 + } from "@/generated/graphql"; 14 17 import { 15 18 type EducationFormData, 16 19 educationFormSchema, ··· 20 23 interface EducationFormProps { 21 24 onSuccess?: () => void; 22 25 onCancel?: () => void; 26 + initialData?: EducationFormData; 27 + educationId?: string; 23 28 } 24 29 25 - export const EducationForm = ({ onSuccess, onCancel }: EducationFormProps) => { 26 - const { mutateAsync: createEducation, isPending: loading } = 30 + export const EducationForm = ({ 31 + onSuccess, 32 + onCancel, 33 + initialData, 34 + educationId, 35 + }: EducationFormProps) => { 36 + const isEditMode = !!educationId; 37 + const { mutateAsync: createEducation, isPending: createLoading } = 27 38 useCreateEducationMutation(); 39 + const { mutateAsync: updateEducation, isPending: updateLoading } = 40 + useUpdateEducationMutation(); 41 + const loading = createLoading || updateLoading; 28 42 const queryClient = useQueryClient(); 29 43 const { showSuccess, showError } = useToast(); 30 44 31 45 const startDateId = useId(); 32 46 const endDateId = useId(); 33 47 34 - const [formData, setFormData] = useState<EducationFormData>({ 35 - institutionId: "", 36 - degree: "", 37 - fieldOfStudy: "", 38 - startDate: null, 39 - endDate: null, 40 - description: "", 41 - isCurrentEducation: false, 42 - skillIds: [], 43 - }); 48 + const [formData, setFormData] = useState<EducationFormData>( 49 + initialData ?? { 50 + institutionId: "", 51 + degree: "", 52 + fieldOfStudy: "", 53 + startDate: null, 54 + endDate: null, 55 + description: "", 56 + isCurrentEducation: false, 57 + skillIds: [], 58 + }, 59 + ); 44 60 45 61 const [errors, setErrors] = useState< 46 62 Partial<Record<keyof EducationFormData, string>> ··· 49 65 const handleSubmit = async (e: React.FormEvent) => { 50 66 e.preventDefault(); 51 67 52 - // Validate form using Zod 53 68 const result = educationFormSchema.safeParse(formData); 54 69 55 70 if (!result.success) { ··· 65 80 66 81 setErrors({}); 67 82 83 + const variables = { 84 + institutionId: formData.institutionId, 85 + degree: formData.degree, 86 + fieldOfStudy: formData.fieldOfStudy || null, 87 + startDate: formData.startDate || new Date(), 88 + endDate: formData.isCurrentEducation ? null : formData.endDate || null, 89 + description: formData.description || null, 90 + skillIds: 91 + formData.skillIds && formData.skillIds.length > 0 92 + ? formData.skillIds 93 + : null, 94 + }; 95 + 68 96 try { 69 - await createEducation({ 70 - institutionId: formData.institutionId, 71 - degree: formData.degree, 72 - fieldOfStudy: formData.fieldOfStudy || null, 73 - startDate: formData.startDate || new Date(), 74 - endDate: formData.isCurrentEducation ? null : formData.endDate || null, 75 - description: formData.description || null, 76 - skillIds: 77 - formData.skillIds && formData.skillIds.length > 0 78 - ? formData.skillIds 79 - : null, 80 - }); 97 + if (isEditMode) { 98 + await updateEducation({ id: educationId, ...variables }); 99 + } else { 100 + await createEducation(variables); 101 + } 81 102 82 - // Invalidate and refetch education queries 83 103 await queryClient.invalidateQueries({ 84 104 queryKey: ["MeEducation"], 85 105 }); 86 106 87 107 showSuccess( 88 - "Education Created", 89 - "Your education has been successfully added.", 108 + isEditMode ? "Education Updated" : "Education Created", 109 + isEditMode 110 + ? "Your education has been successfully updated." 111 + : "Your education has been successfully added.", 90 112 ); 91 113 92 114 onSuccess?.(); 93 115 } catch (_error) { 94 116 showError( 95 - "Failed to Create Education", 96 - "There was an error creating the education. Please try again.", 117 + isEditMode 118 + ? "Failed to Update Education" 119 + : "Failed to Create Education", 120 + "There was an error saving the education. Please try again.", 97 121 ); 98 122 } 99 123 }; ··· 234 258 !formData.startDate 235 259 } 236 260 > 237 - {loading ? "Creating..." : "Create Education"} 261 + {loading 262 + ? isEditMode 263 + ? "Updating..." 264 + : "Creating..." 265 + : isEditMode 266 + ? "Update Education" 267 + : "Create Education"} 238 268 </Button> 239 269 {onCancel && ( 240 270 <Button type="button" variant="ghost" onClick={onCancel}>
+39
apps/client/src/features/education/queries/update-education.graphql
··· 1 + mutation UpdateEducation( 2 + $id: String! 3 + $institutionId: String 4 + $degree: String 5 + $fieldOfStudy: String 6 + $startDate: DateTime 7 + $endDate: DateTime 8 + $description: String 9 + $skillIds: [String!] 10 + ) { 11 + updateEducation( 12 + id: $id 13 + institutionId: $institutionId 14 + degree: $degree 15 + fieldOfStudy: $fieldOfStudy 16 + startDate: $startDate 17 + endDate: $endDate 18 + description: $description 19 + skillIds: $skillIds 20 + ) { 21 + id 22 + institution { 23 + id 24 + name 25 + } 26 + degree 27 + fieldOfStudy 28 + startDate 29 + endDate 30 + description 31 + skills { 32 + id 33 + name 34 + description 35 + } 36 + createdAt 37 + updatedAt 38 + } 39 + }
+62 -33
apps/client/src/features/job-experience/components/JobExperienceForm.tsx
··· 1 1 import { Button, Calendar, Checkbox, Textarea, useToast } from "@cv/ui"; 2 2 import { useQueryClient } from "@tanstack/react-query"; 3 3 import { useId, useState } from "react"; 4 - import { useCreateJobExperienceMutation } from "@/generated/graphql"; 4 + import { 5 + useCreateJobExperienceMutation, 6 + useUpdateJobExperienceMutation, 7 + } from "@/generated/graphql"; 5 8 import { CompanySelect } from "./CompanySelect"; 6 9 import { 7 10 type JobExperienceFormData, ··· 15 18 interface JobExperienceFormProps { 16 19 onSuccess?: () => void; 17 20 onCancel?: () => void; 21 + initialData?: JobExperienceFormData; 22 + experienceId?: string; 18 23 } 19 24 20 25 export const JobExperienceForm = ({ 21 26 onSuccess, 22 27 onCancel, 28 + initialData, 29 + experienceId, 23 30 }: JobExperienceFormProps) => { 24 - const { mutateAsync: createJobExperience, isPending: loading } = 31 + const isEditMode = !!experienceId; 32 + const { mutateAsync: createJobExperience, isPending: createLoading } = 25 33 useCreateJobExperienceMutation(); 34 + const { mutateAsync: updateJobExperience, isPending: updateLoading } = 35 + useUpdateJobExperienceMutation(); 36 + const loading = createLoading || updateLoading; 26 37 const queryClient = useQueryClient(); 27 38 const { showSuccess, showError } = useToast(); 28 39 29 40 const startDateId = useId(); 30 41 const endDateId = useId(); 31 42 32 - const [formData, setFormData] = useState<JobExperienceFormData>({ 33 - companyId: "", 34 - roleId: "", 35 - levelId: "", 36 - startDate: null, 37 - endDate: null, 38 - description: "", 39 - skillIds: [], 40 - isCurrentPosition: false, 41 - }); 43 + const [formData, setFormData] = useState<JobExperienceFormData>( 44 + initialData ?? { 45 + companyId: "", 46 + roleId: "", 47 + levelId: "", 48 + startDate: null, 49 + endDate: null, 50 + description: "", 51 + skillIds: [], 52 + isCurrentPosition: false, 53 + }, 54 + ); 42 55 43 56 const [errors, setErrors] = useState< 44 57 Partial<Record<keyof JobExperienceFormData, string>> ··· 47 60 const handleSubmit = async (e: React.FormEvent) => { 48 61 e.preventDefault(); 49 62 50 - // Validate form using Zod 51 63 const result = jobExperienceFormSchema.safeParse(formData); 52 64 53 65 if (!result.success) { ··· 65 77 66 78 setErrors({}); 67 79 80 + const variables = { 81 + companyId: formData.companyId, 82 + roleId: formData.roleId, 83 + levelId: formData.levelId, 84 + startDate: formData.startDate || new Date(), 85 + endDate: formData.isCurrentPosition ? null : formData.endDate, 86 + description: formData.description || null, 87 + skillIds: 88 + formData.skillIds && formData.skillIds.length > 0 89 + ? formData.skillIds 90 + : null, 91 + }; 92 + 68 93 try { 69 - await createJobExperience({ 70 - companyId: formData.companyId, 71 - roleId: formData.roleId, 72 - levelId: formData.levelId, 73 - startDate: formData.startDate || new Date(), 74 - endDate: formData.isCurrentPosition ? null : formData.endDate, 75 - description: formData.description || null, 76 - skillIds: 77 - formData.skillIds && formData.skillIds.length > 0 78 - ? formData.skillIds 79 - : null, 80 - }); 94 + if (isEditMode) { 95 + await updateJobExperience({ id: experienceId, ...variables }); 96 + } else { 97 + await createJobExperience(variables); 98 + } 81 99 82 - // Invalidate and refetch job experience queries 83 100 await queryClient.invalidateQueries({ 84 101 queryKey: ["MeJobExperience"], 85 102 }); 86 103 87 104 showSuccess( 88 - "Job Experience Created", 89 - "Your job experience has been successfully added.", 105 + isEditMode ? "Job Experience Updated" : "Job Experience Created", 106 + isEditMode 107 + ? "Your job experience has been successfully updated." 108 + : "Your job experience has been successfully added.", 90 109 ); 91 110 92 111 onSuccess?.(); 93 112 } catch (_error) { 94 113 showError( 95 - "Failed to Create Job Experience", 96 - "There was an error creating the job experience. Please try again.", 114 + isEditMode 115 + ? "Failed to Update Job Experience" 116 + : "Failed to Create Job Experience", 117 + "There was an error saving the job experience. Please try again.", 97 118 ); 98 119 } 99 120 }; ··· 119 140 <div className="max-w-2xl mx-auto"> 120 141 <div className="mb-6"> 121 142 <h2 className="text-xl font-semibold text-ctp-text mb-2"> 122 - Create Job Experience 143 + {isEditMode ? "Edit Job Experience" : "Create Job Experience"} 123 144 </h2> 124 145 <p className="text-ctp-subtext0"> 125 - Add a new job experience to your profile 146 + {isEditMode 147 + ? "Update your job experience details" 148 + : "Add a new job experience to your profile"} 126 149 </p> 127 150 </div> 128 151 ··· 237 260 238 261 <div className="flex gap-3 pt-6"> 239 262 <Button type="submit" disabled={loading} className="flex-1"> 240 - {loading ? "Creating..." : "Create Experience"} 263 + {loading 264 + ? isEditMode 265 + ? "Updating..." 266 + : "Creating..." 267 + : isEditMode 268 + ? "Update Experience" 269 + : "Create Experience"} 241 270 </Button> 242 271 243 272 {onCancel && (
+44
apps/client/src/features/job-experience/queries/update-job-experience.graphql
··· 1 + mutation UpdateJobExperience( 2 + $id: String! 3 + $companyId: String 4 + $roleId: String 5 + $levelId: String 6 + $startDate: DateTime 7 + $endDate: DateTime 8 + $description: String 9 + $skillIds: [String!] 10 + ) { 11 + updateJobExperience( 12 + id: $id 13 + companyId: $companyId 14 + roleId: $roleId 15 + levelId: $levelId 16 + startDate: $startDate 17 + endDate: $endDate 18 + description: $description 19 + skillIds: $skillIds 20 + ) { 21 + id 22 + company { 23 + id 24 + name 25 + } 26 + role { 27 + id 28 + name 29 + } 30 + level { 31 + id 32 + name 33 + } 34 + startDate 35 + endDate 36 + description 37 + skills { 38 + id 39 + name 40 + } 41 + createdAt 42 + updatedAt 43 + } 44 + }
+66
apps/client/src/pages/EditEducationPage.tsx
··· 1 + import { PageHeader, Placeholder } from "@cv/ui"; 2 + import { useNavigate, useParams } from "react-router-dom"; 3 + import { EducationForm } from "@/features/education/components/EducationForm"; 4 + import type { EducationFormData } from "@/features/education/components/education.schema"; 5 + import { useMeEducationQuery } from "@/generated/graphql"; 6 + 7 + export default function EditEducationPage() { 8 + const { id } = useParams<{ id: string }>(); 9 + const navigate = useNavigate(); 10 + const { data, isPending } = useMeEducationQuery(); 11 + 12 + if (isPending) { 13 + return ( 14 + <div className="space-y-6"> 15 + <PageHeader 16 + title="Edit Education" 17 + description="Update your education details" 18 + /> 19 + <Placeholder variant="loading" message="Loading education..." /> 20 + </div> 21 + ); 22 + } 23 + 24 + const education = data?.me?.educationHistory?.edges 25 + ?.map((edge) => edge.node) 26 + .find((edu) => edu.id === id); 27 + 28 + if (!education) { 29 + return ( 30 + <div className="space-y-6"> 31 + <PageHeader 32 + title="Edit Education" 33 + description="Update your education details" 34 + /> 35 + <Placeholder variant="error" message="Education not found" /> 36 + </div> 37 + ); 38 + } 39 + 40 + const initialData: EducationFormData = { 41 + institutionId: education.institution.id, 42 + degree: education.degree, 43 + fieldOfStudy: education.fieldOfStudy ?? "", 44 + startDate: new Date(education.startDate), 45 + endDate: education.endDate ? new Date(education.endDate) : null, 46 + description: education.description ?? "", 47 + isCurrentEducation: !education.endDate, 48 + skillIds: education.skills?.map((s) => s.id) ?? [], 49 + }; 50 + 51 + return ( 52 + <div className="space-y-6"> 53 + <PageHeader 54 + title="Edit Education" 55 + description="Update your education details" 56 + /> 57 + 58 + <EducationForm 59 + initialData={initialData} 60 + educationId={id} 61 + onSuccess={() => navigate("/education")} 62 + onCancel={() => navigate("/education")} 63 + /> 64 + </div> 65 + ); 66 + }
+66
apps/client/src/pages/EditJobExperiencePage.tsx
··· 1 + import { PageHeader, Placeholder } from "@cv/ui"; 2 + import { useNavigate, useParams } from "react-router-dom"; 3 + import { JobExperienceForm } from "@/features/job-experience/components/JobExperienceForm"; 4 + import type { JobExperienceFormData } from "@/features/job-experience/components/jobExperience.schema"; 5 + import { useMeJobExperienceQuery } from "@/generated/graphql"; 6 + 7 + export default function EditJobExperiencePage() { 8 + const { id } = useParams<{ id: string }>(); 9 + const navigate = useNavigate(); 10 + const { data, isPending } = useMeJobExperienceQuery(); 11 + 12 + if (isPending) { 13 + return ( 14 + <div className="space-y-6"> 15 + <PageHeader 16 + title="Edit Job Experience" 17 + description="Update your job experience details" 18 + /> 19 + <Placeholder variant="loading" message="Loading job experience..." /> 20 + </div> 21 + ); 22 + } 23 + 24 + const experience = data?.me?.experience?.edges 25 + ?.map((edge) => edge.node) 26 + .find((exp) => exp.id === id); 27 + 28 + if (!experience) { 29 + return ( 30 + <div className="space-y-6"> 31 + <PageHeader 32 + title="Edit Job Experience" 33 + description="Update your job experience details" 34 + /> 35 + <Placeholder variant="error" message="Job experience not found" /> 36 + </div> 37 + ); 38 + } 39 + 40 + const initialData: JobExperienceFormData = { 41 + companyId: experience.company.id, 42 + roleId: experience.role.id, 43 + levelId: experience.level.id, 44 + startDate: new Date(experience.startDate), 45 + endDate: experience.endDate ? new Date(experience.endDate) : null, 46 + description: experience.description ?? "", 47 + skillIds: experience.skills?.map((s) => s.id) ?? [], 48 + isCurrentPosition: !experience.endDate, 49 + }; 50 + 51 + return ( 52 + <div className="space-y-6"> 53 + <PageHeader 54 + title="Edit Job Experience" 55 + description="Update your job experience details" 56 + /> 57 + 58 + <JobExperienceForm 59 + initialData={initialData} 60 + experienceId={id} 61 + onSuccess={() => navigate("/job-experience")} 62 + onCancel={() => navigate("/job-experience")} 63 + /> 64 + </div> 65 + ); 66 + }
+3 -4
apps/client/src/pages/EducationPage.tsx
··· 6 6 7 7 export default function EducationPage() { 8 8 const { data, isPending: loading, error, refetch } = useMeEducationQuery(); 9 - const { showInfo, showError } = useToast(); 9 + const { showError } = useToast(); 10 10 const navigate = useNavigate(); 11 11 12 12 const handleEdit = ( 13 - _education: NonNullable< 13 + education: NonNullable< 14 14 NonNullable<MeEducationQuery["me"]>["educationHistory"] 15 15 >["edges"][number]["node"], 16 16 ) => { 17 - // TODO: Implement edit functionality 18 - showInfo("Edit Feature", "Edit functionality is coming soon!"); 17 + navigate(`/education/edit/${education.id}`); 19 18 }; 20 19 21 20 const handleDelete = async () => {
+3 -4
apps/client/src/pages/JobExperiencePage.tsx
··· 11 11 error, 12 12 refetch, 13 13 } = useMeJobExperienceQuery(); 14 - const { showInfo, showError } = useToast(); 14 + const { showError } = useToast(); 15 15 const navigate = useNavigate(); 16 16 17 17 const handleEdit = ( 18 - _experience: NonNullable< 18 + experience: NonNullable< 19 19 NonNullable<MeJobExperienceQuery["me"]>["experience"] 20 20 >["edges"][number]["node"], 21 21 ) => { 22 - // TODO: Implement edit functionality 23 - showInfo("Edit Feature", "Edit functionality is coming soon!"); 22 + navigate(`/job-experience/edit/${experience.id}`); 24 23 }; 25 24 26 25 const handleDelete = async () => {
+20
apps/client/src/router/AppRouter.tsx
··· 20 20 const CreateJobExperiencePage = lazy( 21 21 () => import("@/pages/CreateJobExperiencePage"), 22 22 ); 23 + const EditEducationPage = lazy(() => import("@/pages/EditEducationPage")); 24 + const EditJobExperiencePage = lazy( 25 + () => import("@/pages/EditJobExperiencePage"), 26 + ); 23 27 const CreateVacancyPage = lazy(() => import("@/pages/CreateVacancyPage")); 24 28 const CVsPage = lazy(() => 25 29 import("@/pages/CVsPage").then((m) => ({ default: m.CVsPage })), ··· 148 152 } 149 153 /> 150 154 <Route 155 + path="job-experience/edit/:id" 156 + element={ 157 + <Suspense fallback={<PageLoader />}> 158 + <EditJobExperiencePage /> 159 + </Suspense> 160 + } 161 + /> 162 + <Route 151 163 path="education" 152 164 element={ 153 165 <Suspense fallback={<PageLoader />}> ··· 160 172 element={ 161 173 <Suspense fallback={<PageLoader />}> 162 174 <CreateEducationPage /> 175 + </Suspense> 176 + } 177 + /> 178 + <Route 179 + path="education/edit/:id" 180 + element={ 181 + <Suspense fallback={<PageLoader />}> 182 + <EditEducationPage /> 163 183 </Suspense> 164 184 } 165 185 />