diff --git a/src/components/Dashboard/Profile/sections/OpportunityDetails/OpportunityDetailsDisplay.tsx b/src/components/Dashboard/Profile/sections/OpportunityDetails/OpportunityDetailsDisplay.tsx index 6354a4b8..96248a39 100644 --- a/src/components/Dashboard/Profile/sections/OpportunityDetails/OpportunityDetailsDisplay.tsx +++ b/src/components/Dashboard/Profile/sections/OpportunityDetails/OpportunityDetailsDisplay.tsx @@ -23,7 +23,7 @@ export function OpportunityDetailsDisplay({ opportunity }: Props) { const isEventType = opp.volunteerType === VolunteerStateTypeType.EVENTS; const mainCommunication = formatLanguagesByPurpose(opp.languages, LangPurpose.GENERAL, t); - const residentsSpeak = formatLanguagesByPurpose(opp.languages, LangPurpose.RECIPIENT, t); + const residentsSpeak = formatLanguagesByPurpose(opp.languages, [LangPurpose.RECIPIENT, LangPurpose.TRANSLATION], t); const schedule = formatAvailability(opp.availability, t); const activities = extractOptionTitles(opp.activities, lang); const skills = extractOptionTitles(opp.skills, lang); diff --git a/src/components/Dashboard/Profile/sections/OpportunityDetails/OpportunityDetailsEdit.tsx b/src/components/Dashboard/Profile/sections/OpportunityDetails/OpportunityDetailsEdit.tsx index 4c9b2e65..42b6730a 100644 --- a/src/components/Dashboard/Profile/sections/OpportunityDetails/OpportunityDetailsEdit.tsx +++ b/src/components/Dashboard/Profile/sections/OpportunityDetails/OpportunityDetailsEdit.tsx @@ -89,14 +89,20 @@ export function OpportunityDetailsEdit({ opportunity, onCancel }: Props) { })); const generalLangs = opp.languages.filter((l) => l.purpose === LangPurpose.GENERAL); - const recipientLangs = opp.languages.filter((l) => l.purpose === LangPurpose.RECIPIENT); + const seenResidents = new Set(); + const residentsLangs = opp.languages.filter((l) => { + if (l.purpose !== LangPurpose.RECIPIENT && l.purpose !== LangPurpose.TRANSLATION) return false; + if (seenResidents.has(l.id)) return false; + seenResidents.add(l.id); + return true; + }); const schema = createOpportunityDetailsSchema(t); const { control, handleSubmit, reset, - formState: { errors, isDirty, isValid }, + formState: { errors, isValid }, } = useForm({ resolver: zodResolver(schema), mode: "onChange", @@ -104,7 +110,7 @@ export function OpportunityDetailsEdit({ opportunity, onCancel }: Props) { description: opp.description ?? "", numberOfVolunteers: String(opp.numberOfVolunteers ?? ""), mainCommunication: languagesToFormValues(generalLangs, t), - residentsSpeak: languagesToFormValues(recipientLangs, t), + residentsSpeak: languagesToFormValues(residentsLangs, t), availability: isEventType ? undefined : apiToFormAvailability(opp.availability), eventDate: null, eventTime: "", @@ -324,7 +330,7 @@ export function OpportunityDetailsEdit({ opportunity, onCancel }: Props) { onClick={handleSubmit(onSubmit)} width="auto" padding="var(--volunteer-profile-section-card-header-button-padding)" - disabled={!isDirty || !isValid} + disabled={!isValid} /> diff --git a/src/components/Dashboard/Profile/sections/OpportunityDetails/formatters.ts b/src/components/Dashboard/Profile/sections/OpportunityDetails/formatters.ts index 86f034bb..9e1fdf00 100644 --- a/src/components/Dashboard/Profile/sections/OpportunityDetails/formatters.ts +++ b/src/components/Dashboard/Profile/sections/OpportunityDetails/formatters.ts @@ -3,15 +3,25 @@ import { LanguageObject } from "@/types"; import { TFunction } from "i18next"; import { ApiLanguage, Lang, LangPurpose, OptionById } from "need4deed-sdk"; -export function formatLanguagesByPurpose(languages: ApiLanguage[], purpose: LangPurpose, t: TFunction): string { - const filtered = languages.filter((lang) => lang.purpose === purpose); +export function formatLanguagesByPurpose( + languages: ApiLanguage[], + purposes: LangPurpose | LangPurpose[], + t: TFunction, +): string { + const purposeSet = new Set(Array.isArray(purposes) ? purposes : [purposes]); + const seen = new Set(); + const filtered = languages.filter((lang) => { + if (!lang.purpose || !purposeSet.has(lang.purpose)) return false; + if (seen.has(lang.id)) return false; + seen.add(lang.id); + return true; + }); if (filtered.length === 0) return EMPTY_PLACEHOLDER_VALUE; return filtered .map((lang) => { const key = `languageNames.${lang.title.toLowerCase()}`; const translated = t(key); - const hasTranslation = translated !== key; - return hasTranslation ? translated : lang.title; + return translated !== key ? translated : lang.title; }) .join(", "); } diff --git a/src/components/Dashboard/Profile/sections/OpportunityDetails/opportunityDetailsSchema.ts b/src/components/Dashboard/Profile/sections/OpportunityDetails/opportunityDetailsSchema.ts index 9cfa388b..3a961cea 100644 --- a/src/components/Dashboard/Profile/sections/OpportunityDetails/opportunityDetailsSchema.ts +++ b/src/components/Dashboard/Profile/sections/OpportunityDetails/opportunityDetailsSchema.ts @@ -11,32 +11,17 @@ const languageObjectSchema = z.object({ level: z.union([z.nativeEnum(LanguageLevel), z.literal("")]), }); -const languagesValidator = (t: (key: string) => string) => - z.array(languageObjectSchema).superRefine((languages, ctx) => { - const hasCompleteRow = languages.some((lang) => lang.language !== ""); - if (!hasCompleteRow) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: t(`${i18nPrefix}.languageRequired`), - }); - } - }); export const createOpportunityDetailsSchema = (t: (key: string) => string) => z.object({ - description: z - .string() - .min(1, t(`${i18nPrefix}.descriptionRequired`)) - .max(MAX_DESCRIPTION_LENGTH, t(`${i18nPrefix}.descriptionTooLong`)), - numberOfVolunteers: z.string().refine((val) => val !== "" && val !== "0", { - message: t(`${i18nPrefix}.numberOfVolunteersRequired`), - }), - mainCommunication: languagesValidator(t), - residentsSpeak: languagesValidator(t), + description: z.string().max(MAX_DESCRIPTION_LENGTH, t(`${i18nPrefix}.descriptionTooLong`)), + numberOfVolunteers: z.string(), + mainCommunication: z.array(languageObjectSchema), + residentsSpeak: z.array(languageObjectSchema), availability: z.custom().nullable().optional(), eventDate: z.date().nullable().optional(), eventTime: z.string().optional(), - activities: z.array(z.string()).min(1, t(`${i18nPrefix}.activitiesRequired`)), + activities: z.array(z.string()), skills: z.array(z.string()), });