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(job-experience): add delete functionality with toast notifications

- Add edit/delete buttons to job experience cards
- Add ConfirmationModal for delete confirmation
- Integrate GraphQL delete mutation with toast notifications
- Add loading states for delete operations
- Reorganize GraphQL operations by feature
- Add success/error toast notifications for delete operations
- Update cursor rules for conventional commits and Docker seeding

+279 -4
+107
apps/client/src/components/ConfirmationModal.tsx
··· 1 + import { Fragment } from "react"; 2 + 3 + interface ConfirmationModalProps { 4 + isOpen: boolean; 5 + onClose: () => void; 6 + onConfirm: () => void; 7 + title: string; 8 + message: string; 9 + confirmText?: string; 10 + cancelText?: string; 11 + variant?: "danger" | "warning" | "info"; 12 + } 13 + 14 + /** 15 + * Reusable confirmation modal component 16 + */ 17 + export default function ConfirmationModal({ 18 + isOpen, 19 + onClose, 20 + onConfirm, 21 + title, 22 + message, 23 + confirmText = "Confirm", 24 + cancelText = "Cancel", 25 + variant = "danger", 26 + }: ConfirmationModalProps) { 27 + if (!isOpen) return null; 28 + 29 + const getVariantStyles = () => { 30 + switch (variant) { 31 + case "danger": 32 + return { 33 + confirmButton: "bg-red-600 hover:bg-red-700 focus:ring-red-500", 34 + icon: "⚠️", 35 + }; 36 + case "warning": 37 + return { 38 + confirmButton: 39 + "bg-yellow-600 hover:bg-yellow-700 focus:ring-yellow-500", 40 + icon: "⚠️", 41 + }; 42 + case "info": 43 + return { 44 + confirmButton: "bg-blue-600 hover:bg-blue-700 focus:ring-blue-500", 45 + icon: "ℹ️", 46 + }; 47 + default: 48 + return { 49 + confirmButton: "bg-red-600 hover:bg-red-700 focus:ring-red-500", 50 + icon: "⚠️", 51 + }; 52 + } 53 + }; 54 + 55 + const styles = getVariantStyles(); 56 + 57 + return ( 58 + <Fragment> 59 + {/* Backdrop */} 60 + <div 61 + className="fixed inset-0 bg-black bg-opacity-50 z-40 transition-opacity" 62 + onClick={onClose} 63 + onKeyDown={(e) => { 64 + if (e.key === "Escape") { 65 + onClose(); 66 + } 67 + }} 68 + role="button" 69 + tabIndex={0} 70 + /> 71 + 72 + {/* Modal */} 73 + <div className="fixed inset-0 z-50 flex items-center justify-center p-4"> 74 + <div className="bg-white rounded-lg shadow-xl max-w-md w-full transform transition-all"> 75 + <div className="p-6"> 76 + {/* Header */} 77 + <div className="flex items-center gap-3 mb-4"> 78 + <span className="text-2xl">{styles.icon}</span> 79 + <h3 className="text-lg font-semibold text-gray-900">{title}</h3> 80 + </div> 81 + 82 + {/* Message */} 83 + <p className="text-gray-700 mb-6 leading-relaxed">{message}</p> 84 + 85 + {/* Actions */} 86 + <div className="flex gap-3 justify-end"> 87 + <button 88 + type="button" 89 + onClick={onClose} 90 + className="px-4 py-2 text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-md font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2" 91 + > 92 + {cancelText} 93 + </button> 94 + <button 95 + type="button" 96 + onClick={onConfirm} 97 + className={`px-4 py-2 text-white rounded-md font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 ${styles.confirmButton}`} 98 + > 99 + {confirmText} 100 + </button> 101 + </div> 102 + </div> 103 + </div> 104 + </div> 105 + </Fragment> 106 + ); 107 + }
+112
apps/client/src/features/job-experience/components/JobExperienceCard.tsx
··· 1 + import { useState } from "react"; 2 + import ConfirmationModal from "@/components/ConfirmationModal"; 1 3 import type { MeJobExperienceQuery } from "@/generated/graphql"; 2 4 import { calculateDuration, formatDateRange } from "@/utils/dateUtils"; 3 5 4 6 interface JobExperienceCardProps { 5 7 experience: MeJobExperienceQuery["myEmploymentHistory"][0]; 8 + onEdit?: (experience: MeJobExperienceQuery["myEmploymentHistory"][0]) => void; 9 + onDelete?: (experienceId: string) => void; 10 + isDeleting?: boolean; 6 11 } 7 12 8 13 /** ··· 10 15 */ 11 16 export default function JobExperienceCard({ 12 17 experience, 18 + onEdit, 19 + onDelete, 20 + isDeleting = false, 13 21 }: JobExperienceCardProps) { 22 + const [showDeleteModal, setShowDeleteModal] = useState(false); 23 + 24 + const handleEdit = () => { 25 + onEdit?.(experience); 26 + }; 27 + 28 + const handleDeleteClick = () => { 29 + setShowDeleteModal(true); 30 + }; 31 + 32 + const handleDeleteConfirm = () => { 33 + onDelete?.(experience.id); 34 + setShowDeleteModal(false); 35 + }; 36 + 37 + const handleDeleteCancel = () => { 38 + setShowDeleteModal(false); 39 + }; 14 40 return ( 15 41 <div className="bg-white rounded-lg border-2 border-gray-200 p-6 shadow-lg hover:shadow-xl transition-all duration-200"> 16 42 <div className="flex justify-between items-start mb-4"> ··· 50 76 </span> 51 77 </div> 52 78 </div> 79 + 80 + {/* Action buttons */} 81 + <div className="flex gap-2 ml-4"> 82 + <button 83 + type="button" 84 + onClick={handleEdit} 85 + className="p-2 text-gray-600 hover:text-blue-600 hover:bg-blue-50 rounded-md transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2" 86 + title="Edit experience" 87 + > 88 + <svg 89 + className="w-4 h-4" 90 + fill="none" 91 + stroke="currentColor" 92 + viewBox="0 0 24 24" 93 + > 94 + <title>Edit icon</title> 95 + <path 96 + strokeLinecap="round" 97 + strokeLinejoin="round" 98 + strokeWidth={2} 99 + d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" 100 + /> 101 + </svg> 102 + </button> 103 + <button 104 + type="button" 105 + onClick={handleDeleteClick} 106 + disabled={isDeleting} 107 + className={`p-2 rounded-md transition-colors focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 ${ 108 + isDeleting 109 + ? "text-gray-400 cursor-not-allowed" 110 + : "text-gray-600 hover:text-red-600 hover:bg-red-50" 111 + }`} 112 + title={isDeleting ? "Deleting..." : "Delete experience"} 113 + > 114 + {isDeleting ? ( 115 + <svg 116 + className="w-4 h-4 animate-spin" 117 + fill="none" 118 + viewBox="0 0 24 24" 119 + > 120 + <title>Loading icon</title> 121 + <circle 122 + className="opacity-25" 123 + cx="12" 124 + cy="12" 125 + r="10" 126 + stroke="currentColor" 127 + strokeWidth="4" 128 + /> 129 + <path 130 + className="opacity-75" 131 + fill="currentColor" 132 + d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" 133 + /> 134 + </svg> 135 + ) : ( 136 + <svg 137 + className="w-4 h-4" 138 + fill="none" 139 + stroke="currentColor" 140 + viewBox="0 0 24 24" 141 + > 142 + <title>Delete icon</title> 143 + <path 144 + strokeLinecap="round" 145 + strokeLinejoin="round" 146 + strokeWidth={2} 147 + d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" 148 + /> 149 + </svg> 150 + )} 151 + </button> 152 + </div> 53 153 </div> 54 154 55 155 {experience.description && ( ··· 77 177 </div> 78 178 </div> 79 179 )} 180 + 181 + {/* Delete confirmation modal */} 182 + <ConfirmationModal 183 + isOpen={showDeleteModal} 184 + onClose={handleDeleteCancel} 185 + onConfirm={handleDeleteConfirm} 186 + title="Delete Job Experience" 187 + message={`Are you sure you want to delete your experience as ${experience.role.name} at ${experience.company.name}? This action cannot be undone.`} 188 + confirmText="Delete" 189 + cancelText="Cancel" 190 + variant="danger" 191 + /> 80 192 </div> 81 193 ); 82 194 }
+13 -1
apps/client/src/features/job-experience/components/JobExperienceList.tsx
··· 3 3 4 4 interface JobExperienceListProps { 5 5 experiences: MeJobExperienceQuery["myEmploymentHistory"]; 6 + onEdit?: (experience: MeJobExperienceQuery["myEmploymentHistory"][0]) => void; 7 + onDelete?: (experienceId: string) => void; 8 + isDeleting?: boolean; 6 9 } 7 10 8 11 /** ··· 10 13 */ 11 14 export default function JobExperienceList({ 12 15 experiences, 16 + onEdit, 17 + onDelete, 18 + isDeleting = false, 13 19 }: JobExperienceListProps) { 14 20 return ( 15 21 <div className="space-y-6"> 16 22 {experiences.map((experience) => ( 17 - <JobExperienceCard key={experience.id} experience={experience} /> 23 + <JobExperienceCard 24 + key={experience.id} 25 + experience={experience} 26 + onEdit={onEdit} 27 + onDelete={onDelete} 28 + isDeleting={isDeleting} 29 + /> 18 30 ))} 19 31 </div> 20 32 );
apps/client/src/graphql/app.graphql apps/client/src/graphql/features/app/app.graphql
apps/client/src/graphql/auth.graphql apps/client/src/graphql/features/auth/auth.graphql
+3
apps/client/src/graphql/features/job-experience/delete-job-experience.graphql
··· 1 + mutation DeleteJobExperience($id: String!) { 2 + deleteJobExperience(id: $id) 3 + }
apps/client/src/graphql/me.graphql apps/client/src/graphql/features/user/me.graphql
apps/client/src/graphql/queries/me-job-experience.graphql apps/client/src/graphql/features/job-experience/me-job-experience.graphql
+44 -3
apps/client/src/pages/JobExperiencePage.tsx
··· 1 1 import { ErrorDisplay } from "@/components/ErrorBoundary"; 2 + import { useToast } from "@/contexts/ToastContext"; 2 3 import { 3 4 JobExperienceEmpty, 4 5 JobExperienceHeader, 5 6 JobExperienceList, 6 7 JobExperienceLoading, 7 8 } from "@/features/job-experience/components"; 8 - import { useMeJobExperienceQuery } from "@/generated/graphql"; 9 + import type { MeJobExperienceQuery } from "@/generated/graphql"; 10 + import { 11 + useDeleteJobExperienceMutation, 12 + useMeJobExperienceQuery, 13 + } from "@/generated/graphql"; 9 14 10 15 export default function JobExperiencePage() { 11 - const { data, loading, error } = useMeJobExperienceQuery(); 16 + const { data, loading, error, refetch } = useMeJobExperienceQuery(); 17 + const [deleteJobExperience, { loading: deleteLoading }] = 18 + useDeleteJobExperienceMutation(); 19 + const { showSuccess, showError, showInfo } = useToast(); 20 + 21 + const handleEdit = ( 22 + experience: MeJobExperienceQuery["myEmploymentHistory"][0], 23 + ) => { 24 + // TODO: Implement edit functionality 25 + showInfo("Edit Feature", "Edit functionality is coming soon!"); 26 + console.log("Edit experience:", experience); 27 + }; 28 + 29 + const handleDelete = async (experienceId: string) => { 30 + try { 31 + await deleteJobExperience({ 32 + variables: { id: experienceId }, 33 + }); 34 + // Refetch the data to update the UI 35 + await refetch(); 36 + showSuccess( 37 + "Job Experience Deleted", 38 + "The job experience has been successfully removed.", 39 + ); 40 + } catch (error) { 41 + console.error("Failed to delete job experience:", error); 42 + showError( 43 + "Delete Failed", 44 + "Failed to delete the job experience. Please try again.", 45 + ); 46 + } 47 + }; 12 48 13 49 if (loading) { 14 50 return <JobExperienceLoading />; ··· 34 70 <div className="min-h-screen bg-white"> 35 71 <div className="max-w-4xl mx-auto px-4 py-8"> 36 72 <JobExperienceHeader /> 37 - <JobExperienceList experiences={jobExperiences} /> 73 + <JobExperienceList 74 + experiences={jobExperiences} 75 + onEdit={handleEdit} 76 + onDelete={handleDelete} 77 + isDeleting={deleteLoading} 78 + /> 38 79 </div> 39 80 </div> 40 81 );