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.

fix(client): improve a11y in ConfirmationModal and JobExperienceForm

+288 -30
+32 -30
apps/client/src/components/ConfirmationModal.tsx
··· 1 + import { cva } from "class-variance-authority"; 1 2 import { Fragment } from "react"; 3 + 4 + const confirmButtonVariants = cva( 5 + "px-4 py-2 text-white rounded-md font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-1", 6 + { 7 + variants: { 8 + variant: { 9 + danger: "bg-ctp-red hover:bg-ctp-red/90 focus:ring-ctp-red", 10 + warning: "bg-ctp-yellow hover:bg-ctp-yellow/90 focus:ring-ctp-yellow", 11 + info: "bg-ctp-blue hover:bg-ctp-blue/90 focus:ring-ctp-blue", 12 + }, 13 + }, 14 + defaultVariants: { 15 + variant: "danger", 16 + }, 17 + }, 18 + ); 2 19 3 20 interface ConfirmationModalProps { 4 21 isOpen: boolean; ··· 26 43 }: ConfirmationModalProps) { 27 44 if (!isOpen) return null; 28 45 29 - const getVariantStyles = () => { 46 + const getIcon = () => { 30 47 switch (variant) { 31 48 case "danger": 32 - return { 33 - confirmButton: "bg-red-600 hover:bg-red-700 focus:ring-red-500", 34 - icon: "⚠️", 35 - }; 49 + return "⚠️"; 36 50 case "warning": 37 - return { 38 - confirmButton: 39 - "bg-yellow-600 hover:bg-yellow-700 focus:ring-yellow-500", 40 - icon: "⚠️", 41 - }; 51 + return "⚠️"; 42 52 case "info": 43 - return { 44 - confirmButton: "bg-blue-600 hover:bg-blue-700 focus:ring-blue-500", 45 - icon: "ℹ️", 46 - }; 53 + return "ℹ️"; 47 54 default: 48 - return { 49 - confirmButton: "bg-red-600 hover:bg-red-700 focus:ring-red-500", 50 - icon: "⚠️", 51 - }; 55 + return "⚠️"; 52 56 } 53 57 }; 54 58 55 - const styles = getVariantStyles(); 56 - 57 59 return ( 58 60 <Fragment> 59 61 {/* Backdrop */} 60 - <div 61 - className="fixed inset-0 bg-black bg-opacity-50 z-40 transition-opacity" 62 + <button 63 + type="button" 64 + aria-label="Close modal" 65 + className="fixed inset-0 bg-ctp-base/50 z-40 transition-opacity" 62 66 onClick={onClose} 63 67 onKeyDown={(e) => { 64 68 if (e.key === "Escape") { 65 69 onClose(); 66 70 } 67 71 }} 68 - role="button" 69 - tabIndex={0} 70 72 /> 71 73 72 74 {/* Modal */} 73 75 <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"> 76 + <div className="bg-ctp-surface0 border border-ctp-surface1 rounded-lg shadow-xl max-w-md w-full transform transition-all"> 75 77 <div className="p-6"> 76 78 {/* Header */} 77 79 <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 + <span className="text-2xl">{getIcon()}</span> 81 + <h3 className="text-lg font-semibold text-ctp-text">{title}</h3> 80 82 </div> 81 83 82 84 {/* Message */} 83 - <p className="text-gray-700 mb-6 leading-relaxed">{message}</p> 85 + <p className="text-ctp-subtext0 mb-6 leading-relaxed">{message}</p> 84 86 85 87 {/* Actions */} 86 88 <div className="flex gap-3 justify-end"> 87 89 <button 88 90 type="button" 89 91 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" 92 + className="px-4 py-2 text-ctp-subtext0 bg-ctp-surface1 hover:bg-ctp-surface2 rounded-md font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-ctp-surface1 focus:ring-offset-1" 91 93 > 92 94 {cancelText} 93 95 </button> 94 96 <button 95 97 type="button" 96 98 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}`} 99 + className={confirmButtonVariants({ variant })} 98 100 > 99 101 {confirmText} 100 102 </button>
+256
apps/client/src/features/job-experience/components/JobExperienceForm.tsx
··· 1 + import { useState } from "react"; 2 + import { useToast } from "@/contexts/ToastContext"; 3 + import { 4 + useCreateJobExperienceMutation, 5 + useJobExperienceFormDataQuery, 6 + } from "@/generated/graphql"; 7 + import Button from "@/ui/Button"; 8 + import Checkbox from "@/ui/Checkbox"; 9 + import { Select } from "@/ui/Select"; 10 + import Textarea from "@/ui/Textarea"; 11 + import TextInput from "@/ui/TextInput"; 12 + import { 13 + type JobExperienceFormData, 14 + jobExperienceFormSchema, 15 + } from "./jobExperience.schema"; 16 + 17 + interface JobExperienceFormProps { 18 + onSuccess?: () => void; 19 + onCancel?: () => void; 20 + } 21 + 22 + export const JobExperienceForm = ({ 23 + onSuccess, 24 + onCancel, 25 + }: JobExperienceFormProps) => { 26 + const [createJobExperience, { loading }] = useCreateJobExperienceMutation(); 27 + const { showSuccess, showError } = useToast(); 28 + 29 + // Fetch all form data in one query 30 + const { data: formDataQuery } = useJobExperienceFormDataQuery(); 31 + 32 + const [formData, setFormData] = useState<JobExperienceFormData>({ 33 + companyId: "", 34 + roleId: "", 35 + levelId: "", 36 + startDate: "", 37 + endDate: "", 38 + description: "", 39 + skillIds: [], 40 + isCurrentPosition: false, 41 + }); 42 + 43 + const [errors, setErrors] = useState< 44 + Partial<Record<keyof JobExperienceFormData, string>> 45 + >({}); 46 + 47 + const handleSubmit = async (e: React.FormEvent) => { 48 + e.preventDefault(); 49 + 50 + // Validate form using Zod 51 + const result = jobExperienceFormSchema.safeParse(formData); 52 + 53 + if (!result.success) { 54 + const newErrors: Partial<Record<keyof JobExperienceFormData, string>> = 55 + {}; 56 + result.error.errors.forEach((error) => { 57 + if (error.path.length > 0) { 58 + newErrors[error.path[0] as keyof JobExperienceFormData] = 59 + error.message; 60 + } 61 + }); 62 + setErrors(newErrors); 63 + return; 64 + } 65 + 66 + setErrors({}); 67 + 68 + try { 69 + await createJobExperience({ 70 + variables: { 71 + companyId: formData.companyId, 72 + roleId: formData.roleId, 73 + levelId: formData.levelId, 74 + startDate: new Date(formData.startDate), 75 + endDate: formData.isCurrentPosition 76 + ? null 77 + : formData.endDate 78 + ? new Date(formData.endDate) 79 + : null, 80 + description: formData.description || null, 81 + skillIds: 82 + formData.skillIds && formData.skillIds.length > 0 83 + ? formData.skillIds 84 + : null, 85 + }, 86 + }); 87 + 88 + showSuccess( 89 + "Job Experience Created", 90 + "Your job experience has been successfully added.", 91 + ); 92 + 93 + onSuccess?.(); 94 + } catch (error) { 95 + console.error("Error creating job experience:", error); 96 + showError( 97 + "Failed to Create Job Experience", 98 + "There was an error creating the job experience. Please try again.", 99 + ); 100 + } 101 + }; 102 + 103 + const handleSkillToggle = (skillId: string) => { 104 + setFormData((prev) => ({ 105 + ...prev, 106 + skillIds: (prev.skillIds || []).includes(skillId) 107 + ? (prev.skillIds || []).filter((id) => id !== skillId) 108 + : [...(prev.skillIds || []), skillId], 109 + })); 110 + }; 111 + 112 + const handleCurrentPositionToggle = (checked: boolean) => { 113 + setFormData((prev) => ({ 114 + ...prev, 115 + isCurrentPosition: checked, 116 + endDate: checked ? "" : prev.endDate, 117 + })); 118 + }; 119 + 120 + const companies = formDataQuery?.companies || []; 121 + const roles = formDataQuery?.roles || []; 122 + const levels = formDataQuery?.levels || []; 123 + const skills = formDataQuery?.skills || []; 124 + 125 + return ( 126 + <div className="max-w-2xl mx-auto"> 127 + <div className="mb-6"> 128 + <h2 className="text-xl font-semibold text-ctp-text mb-2"> 129 + Create Job Experience 130 + </h2> 131 + <p className="text-ctp-subtext0"> 132 + Add a new job experience to your profile 133 + </p> 134 + </div> 135 + 136 + <form onSubmit={handleSubmit} className="space-y-6"> 137 + <div className="grid grid-cols-1 md:grid-cols-2 gap-6"> 138 + <Select 139 + label="Company" 140 + options={companies.map((company) => ({ 141 + value: company.id, 142 + label: company.name, 143 + }))} 144 + value={formData.companyId} 145 + onChange={(value: string) => 146 + setFormData((prev) => ({ ...prev, companyId: value })) 147 + } 148 + placeholder="Select a company" 149 + required 150 + error={errors.companyId} 151 + /> 152 + 153 + <Select 154 + label="Role" 155 + options={roles.map((role) => ({ 156 + value: role.id, 157 + label: role.name, 158 + }))} 159 + value={formData.roleId} 160 + onChange={(value: string) => 161 + setFormData((prev) => ({ ...prev, roleId: value })) 162 + } 163 + placeholder="Select a role" 164 + required 165 + error={errors.roleId} 166 + /> 167 + 168 + <Select 169 + label="Level" 170 + options={levels.map((level) => ({ 171 + value: level.id, 172 + label: level.name, 173 + }))} 174 + value={formData.levelId} 175 + onChange={(value: string) => 176 + setFormData((prev) => ({ ...prev, levelId: value })) 177 + } 178 + placeholder="Select a level" 179 + required 180 + error={errors.levelId} 181 + /> 182 + 183 + <TextInput 184 + label="Start Date" 185 + type="date" 186 + value={formData.startDate} 187 + onChange={(value: string) => 188 + setFormData((prev) => ({ ...prev, startDate: value })) 189 + } 190 + required 191 + error={errors.startDate} 192 + /> 193 + </div> 194 + 195 + <div className="space-y-4"> 196 + <Checkbox 197 + label="This is my current position" 198 + checked={formData.isCurrentPosition} 199 + onChange={handleCurrentPositionToggle} 200 + /> 201 + 202 + {!formData.isCurrentPosition && ( 203 + <TextInput 204 + label="End Date" 205 + type="date" 206 + value={formData.endDate} 207 + onChange={(value: string) => 208 + setFormData((prev) => ({ ...prev, endDate: value })) 209 + } 210 + required={!formData.isCurrentPosition} 211 + error={errors.endDate} 212 + /> 213 + )} 214 + </div> 215 + 216 + <Textarea 217 + label="Description" 218 + value={formData.description} 219 + onChange={(value: string) => 220 + setFormData((prev) => ({ ...prev, description: value })) 221 + } 222 + placeholder="Describe your role, responsibilities, and achievements..." 223 + rows={4} 224 + /> 225 + 226 + <div className="space-y-3"> 227 + <span className="block text-sm font-medium text-ctp-text"> 228 + Skills 229 + </span> 230 + <div className="grid grid-cols-2 md:grid-cols-3 gap-2 max-h-40 overflow-y-auto"> 231 + {skills.map((skill) => ( 232 + <Checkbox 233 + key={skill.id} 234 + label={skill.name} 235 + checked={(formData.skillIds || []).includes(skill.id)} 236 + onChange={() => handleSkillToggle(skill.id)} 237 + /> 238 + ))} 239 + </div> 240 + </div> 241 + 242 + <div className="flex gap-3 pt-6"> 243 + <Button type="submit" disabled={loading} className="flex-1"> 244 + {loading ? "Creating..." : "Create Experience"} 245 + </Button> 246 + 247 + {onCancel && ( 248 + <Button type="button" variant="ghost" onClick={onCancel}> 249 + Cancel 250 + </Button> 251 + )} 252 + </div> 253 + </form> 254 + </div> 255 + ); 256 + };