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.

refactor(client): co-locate Zod schemas with forms and remove validation dir

+275
+27
apps/client/src/features/job-experience/components/jobExperience.schema.ts
··· 1 + import { z } from "zod"; 2 + 3 + export const jobExperienceFormSchema = z 4 + .object({ 5 + companyId: z.string().min(1, "Company is required"), 6 + roleId: z.string().min(1, "Role is required"), 7 + levelId: z.string().min(1, "Level is required"), 8 + startDate: z.string().min(1, "Start date is required"), 9 + endDate: z.string().optional(), 10 + description: z.string().optional(), 11 + skillIds: z.array(z.string()).optional(), 12 + isCurrentPosition: z.boolean().default(false), 13 + }) 14 + .refine( 15 + (data) => { 16 + if (!(data.isCurrentPosition || data.endDate)) { 17 + return false; 18 + } 19 + return true; 20 + }, 21 + { 22 + message: "End date is required for past positions", 23 + path: ["endDate"], 24 + }, 25 + ); 26 + 27 + export type JobExperienceFormData = z.infer<typeof jobExperienceFormSchema>;
+204
apps/client/src/features/vacancies/components/VacancyForm.tsx
··· 1 + import { useState } from "react"; 2 + import { useToast } from "@/contexts/ToastContext"; 3 + import { useCreateVacancyMutation } from "@/generated/graphql"; 4 + import Button from "@/ui/Button"; 5 + import Checkbox from "@/ui/Checkbox"; 6 + import Textarea from "@/ui/Textarea"; 7 + import TextInput from "@/ui/TextInput"; 8 + import { type VacancyFormData, vacancyFormSchema } from "./vacancy.schema"; 9 + 10 + interface VacancyFormProps { 11 + onSuccess?: () => void; 12 + onCancel?: () => void; 13 + } 14 + 15 + export const VacancyForm = ({ onSuccess, onCancel }: VacancyFormProps) => { 16 + const [createVacancy, { loading }] = useCreateVacancyMutation(); 17 + const { showSuccess, showError } = useToast(); 18 + const [formData, setFormData] = useState<VacancyFormData>({ 19 + title: "", 20 + company: "", 21 + description: "", 22 + requirements: "", 23 + location: "", 24 + salary: "", 25 + jobType: "", 26 + applicationUrl: "", 27 + deadline: "", 28 + isActive: true, 29 + }); 30 + const [errors, setErrors] = useState< 31 + Partial<Record<keyof VacancyFormData, string>> 32 + >({}); 33 + 34 + const handleSubmit = async (e: React.FormEvent) => { 35 + e.preventDefault(); 36 + 37 + // Validate form using Zod 38 + const result = vacancyFormSchema.safeParse(formData); 39 + 40 + if (!result.success) { 41 + const newErrors: Partial<Record<keyof VacancyFormData, string>> = {}; 42 + result.error.errors.forEach((error) => { 43 + if (error.path.length > 0) { 44 + newErrors[error.path[0] as keyof VacancyFormData] = error.message; 45 + } 46 + }); 47 + setErrors(newErrors); 48 + return; 49 + } 50 + 51 + setErrors({}); 52 + 53 + try { 54 + await createVacancy({ 55 + variables: { 56 + title: formData.title, 57 + company: formData.company, 58 + ...(formData.description && { description: formData.description }), 59 + ...(formData.requirements && { requirements: formData.requirements }), 60 + ...(formData.location && { location: formData.location }), 61 + ...(formData.salary && { salary: formData.salary }), 62 + ...(formData.jobType && { jobType: formData.jobType }), 63 + ...(formData.applicationUrl && { 64 + applicationUrl: formData.applicationUrl, 65 + }), 66 + ...(formData.deadline && { deadline: new Date(formData.deadline) }), 67 + isActive: formData.isActive, 68 + }, 69 + }); 70 + 71 + // Show success toast 72 + showSuccess( 73 + "Vacancy Created", 74 + `Successfully created vacancy for ${formData.title} at ${formData.company}`, 75 + ); 76 + 77 + // Reset form 78 + setFormData({ 79 + title: "", 80 + company: "", 81 + description: "", 82 + requirements: "", 83 + location: "", 84 + salary: "", 85 + jobType: "", 86 + applicationUrl: "", 87 + deadline: "", 88 + isActive: true, 89 + }); 90 + 91 + onSuccess?.(); 92 + } catch (error) { 93 + console.error("Error creating vacancy:", error); 94 + showError( 95 + "Failed to Create Vacancy", 96 + "There was an error creating the vacancy. Please try again.", 97 + ); 98 + } 99 + }; 100 + 101 + const handleInputChange = (field: string, value: string) => { 102 + setFormData((prev) => ({ ...prev, [field]: value })); 103 + }; 104 + 105 + const handleCheckboxChange = (field: string, checked: boolean) => { 106 + setFormData((prev) => ({ ...prev, [field]: checked })); 107 + }; 108 + 109 + return ( 110 + <form onSubmit={handleSubmit} className="space-y-6"> 111 + <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> 112 + <TextInput 113 + label="Job Title" 114 + value={formData.title} 115 + onChange={(value: string) => handleInputChange("title", value)} 116 + required 117 + error={errors.title} 118 + /> 119 + 120 + <TextInput 121 + label="Company" 122 + value={formData.company} 123 + onChange={(value: string) => handleInputChange("company", value)} 124 + required 125 + error={errors.company} 126 + /> 127 + 128 + <TextInput 129 + label="Location" 130 + value={formData.location} 131 + onChange={(value: string) => handleInputChange("location", value)} 132 + /> 133 + 134 + <TextInput 135 + label="Salary" 136 + value={formData.salary} 137 + onChange={(value: string) => handleInputChange("salary", value)} 138 + /> 139 + 140 + <TextInput 141 + label="Job Type" 142 + value={formData.jobType} 143 + onChange={(value: string) => handleInputChange("jobType", value)} 144 + /> 145 + 146 + <TextInput 147 + label="Application URL" 148 + value={formData.applicationUrl} 149 + onChange={(value: string) => 150 + handleInputChange("applicationUrl", value) 151 + } 152 + error={errors.applicationUrl} 153 + /> 154 + 155 + <div className="md:col-span-2"> 156 + <TextInput 157 + label="Deadline" 158 + type="datetime-local" 159 + value={formData.deadline} 160 + onChange={(value: string) => handleInputChange("deadline", value)} 161 + /> 162 + </div> 163 + </div> 164 + 165 + <Textarea 166 + label="Description" 167 + value={formData.description} 168 + onChange={(value: string) => handleInputChange("description", value)} 169 + rows={4} 170 + placeholder="Job description..." 171 + /> 172 + 173 + <Textarea 174 + label="Requirements" 175 + value={formData.requirements} 176 + onChange={(value: string) => handleInputChange("requirements", value)} 177 + rows={3} 178 + placeholder="Job requirements..." 179 + /> 180 + 181 + <Checkbox 182 + label="Active" 183 + checked={formData.isActive} 184 + onChange={(checked: boolean) => 185 + handleCheckboxChange("isActive", checked) 186 + } 187 + /> 188 + 189 + <div className="flex space-x-4"> 190 + <Button 191 + type="submit" 192 + disabled={loading || !formData.title || !formData.company} 193 + > 194 + {loading ? "Creating..." : "Create Vacancy"} 195 + </Button> 196 + {onCancel && ( 197 + <Button type="button" variant="ghost" onClick={onCancel}> 198 + Cancel 199 + </Button> 200 + )} 201 + </div> 202 + </form> 203 + ); 204 + };
+44
apps/client/src/features/vacancies/components/vacancy.schema.ts
··· 1 + import { z } from "zod"; 2 + 3 + export const vacancyFormSchema = z 4 + .object({ 5 + title: z 6 + .string() 7 + .min(1, "Title is required") 8 + .max(200, "Title must be less than 200 characters"), 9 + company: z 10 + .string() 11 + .min(1, "Company is required") 12 + .max(100, "Company must be less than 100 characters"), 13 + description: z.string().optional(), 14 + requirements: z.string().optional(), 15 + location: z.string().optional(), 16 + salary: z.string().optional(), 17 + jobType: z.string().optional(), 18 + applicationUrl: z 19 + .string() 20 + .url("Must be a valid URL") 21 + .optional() 22 + .or(z.literal("")), 23 + deadline: z.string().optional(), 24 + isActive: z.boolean().default(true), 25 + }) 26 + .refine( 27 + (data) => { 28 + if (data.applicationUrl && data.applicationUrl !== "") { 29 + try { 30 + new URL(data.applicationUrl); 31 + return true; 32 + } catch { 33 + return false; 34 + } 35 + } 36 + return true; 37 + }, 38 + { 39 + message: "Must be a valid URL", 40 + path: ["applicationUrl"], 41 + }, 42 + ); 43 + 44 + export type VacancyFormData = z.infer<typeof vacancyFormSchema>;