From 57ca701c0449046d5d9575790db3bc2d616a93c5 Mon Sep 17 00:00:00 2001 From: OlivierSchulbaum Date: Fri, 21 Aug 2026 19:58:50 +0200 Subject: [PATCH 01/26] New translations es.json (Catalan) [ci skip] --- src/i18n/locales/ca.json | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/src/i18n/locales/ca.json b/src/i18n/locales/ca.json index 6d956172..68796481 100644 --- a/src/i18n/locales/ca.json +++ b/src/i18n/locales/ca.json @@ -35,6 +35,16 @@ } }, "validation": { + "_invalid_type": "", + "_too_big": "", + "too_small": "\"{{ value }}\" és massa curt: s'esperen {{ min }} caràcters o més.", + "invalid_format": "\"{{ value }}\" té un format incorrecte: no s'ajusta al patró regex `{{ pattern }}`.", + "_not_multiple_of": "", + "_unrecognized_keys": "", + "_invalid_union": "", + "_invalid_key": "", + "_invalid_element": "", + "_invalid_value": "", "missingRequiredFields": "Un o més camps obligatoris estan buits", "project": { "terms": { @@ -629,6 +639,13 @@ "project": { "edit": { "title": "Editant projecte: {{ project }}", + "validation": { + "titleBadFormat": "El títol ha de contenir almenys una lletra." + }, + "header": { + "titlePlaceholder": "Títol del projecte", + "unsentChanges": "Hi ha canvis no enviats" + }, "tabs": { "configuration": "Configuració", "campaign": "Informació de campanya", @@ -647,6 +664,11 @@ "secondaryLabel": "Idioma secundari del contingut", "addButton": "Afegir idioma" }, + "release": { + "title": "Data de sortida a campanya", + "subtitle": "Tingues en compte que les campanyes de micromecenatge tenen una durada mínima de 40 dies", + "passed": "Aquest projecte ja ha sortit a campanya i no es pot modificar la seva data de sortida" + }, "rounds": { "title": "Tria les rondes que tindrà la teva campanya", "description": "La configuració predeterminada és de 40 dies cada ronda, pots sol·licitar modificacions al teu assessor si el teu projecte és seleccionat", @@ -872,17 +894,6 @@ }, "continue": "Pas següent", "locationPlaceholder": "Ex.: Carrer Gran Via 28, Madrid" - }, - "errors": { - "session": { - "title": "Sessió expirada" - }, - "storage": { - "title": "Error d'emmagatzematge", - "save_failed": "No s'han pogut desar els canvis", - "quota_exceeded": "Emmagatzematge local ple", - "close": "Tancar" - } } }, "create": { From 7940531a677bf54e839a16c970ed9634c073f043 Mon Sep 17 00:00:00 2001 From: OlivierSchulbaum Date: Fri, 21 Aug 2026 19:58:52 +0200 Subject: [PATCH 02/26] New translations es.json (English) [ci skip] --- src/i18n/locales/en.json | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 434d42e0..d47ca720 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -35,6 +35,16 @@ } }, "validation": { + "_invalid_type": "", + "_too_big": "", + "too_small": "\"{{ value }}\" is too short: {{ min }} or more characters expected.", + "invalid_format": "\"{{ value }}\" has an invalid format: it does not match the regex pattern `{{ pattern }}`.", + "_not_multiple_of": "", + "_unrecognized_keys": "", + "_invalid_union": "", + "_invalid_key": "", + "_invalid_element": "", + "_invalid_value": "", "missingRequiredFields": "One or more required fields are empty", "project": { "terms": { @@ -629,6 +639,13 @@ "project": { "edit": { "title": "Editing project: {{ project }}", + "validation": { + "titleBadFormat": "The title must contain at least one letter." + }, + "header": { + "titlePlaceholder": "Project title", + "unsentChanges": "There are unsent changes" + }, "tabs": { "configuration": "Configuration", "campaign": "Campaign information", @@ -647,6 +664,11 @@ "secondaryLabel": "Secondary content language", "addButton": "Add language" }, + "release": { + "title": "Campaign launch date", + "subtitle": "Please note that crowdfunding campaigns have a minimum duration of 40 days", + "passed": "This project has already launched as a campaign and its launch date cannot be changed" + }, "rounds": { "title": "Choose the rounds your campaign will have", "description": "The default setting is 40 days each round. You can request modifications through your advisor if your project is selected", @@ -872,17 +894,6 @@ }, "continue": "Next step", "locationPlaceholder": "E.g.: Calle Gran Vía 28, Madrid" - }, - "errors": { - "session": { - "title": "Session expired" - }, - "storage": { - "title": "Storage error", - "save_failed": "Changes could not be saved", - "quota_exceeded": "Local storage full", - "close": "Close" - } } }, "create": { From 93fc75b59be9028a081ddf794bf4e33f2ab2dabf Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 24 Aug 2026 10:22:06 +0200 Subject: [PATCH 03/26] fix: /create/project without login callback and cached categories --- src/pages/[...locale]/create/project.astro | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/pages/[...locale]/create/project.astro b/src/pages/[...locale]/create/project.astro index b3454165..f4098f8e 100644 --- a/src/pages/[...locale]/create/project.astro +++ b/src/pages/[...locale]/create/project.astro @@ -5,21 +5,25 @@ import CreateProjectForm from "../../../components/create/CreateProjectForm.svel import { apiCategoriesGetCollection } from "../../../openapi/client"; import { toCollectionItems } from "../../../utils/hydra"; import type { Category } from "../../../openapi/client"; +import { goto } from "../../../utils/navigation"; +import { withoutCache } from "../../../openapi/cacheInterceptor"; const { t, lang, langs, session } = Astro.locals; if (!session) { - return Astro.redirect("/login"); + return goto("/login", { query: { callback: "/create/project" } }); } -const { data: categoriesRaw } = await apiCategoriesGetCollection({ - headers: { "Accept-Language": langs }, -}); +const { data: categoriesRaw } = await withoutCache(() => + apiCategoriesGetCollection({ + headers: { "Accept-Language": langs }, + }), +); const categories = toCollectionItems(categoriesRaw); --- - + From 0251c94d8f1ceec00dca620544a2c5e4fc9b6866 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 24 Aug 2026 11:07:22 +0200 Subject: [PATCH 04/26] fix: make TextInput give value for onInput argument --- src/components/admin/charges/steps/StepCharge.svelte | 3 +-- src/components/library/inputs/TextInput.svelte | 4 ++-- src/components/project/edit/VideoUrlInput.svelte | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/components/admin/charges/steps/StepCharge.svelte b/src/components/admin/charges/steps/StepCharge.svelte index 9dbf6015..8372511a 100644 --- a/src/components/admin/charges/steps/StepCharge.svelte +++ b/src/components/admin/charges/steps/StepCharge.svelte @@ -299,8 +299,7 @@ labelText={$t("pages.admin.charges.headers.amount")} helperText={$t("pages.admin.charges.create.fields.amountHelper")} required={true} - onInput={(e) => - onAmountInput(e.target instanceof HTMLInputElement ? e.target.value : "")} + onInput={(value) => onAmountInput(value.toString())} onBlur={onAmountBlur} /> onSearch?.(e.currentTarget.value)} diff --git a/src/components/library/inputs/TerritoryInput.svelte b/src/components/library/inputs/TerritoryInput.svelte index d9722f89..c756a7f9 100644 --- a/src/components/library/inputs/TerritoryInput.svelte +++ b/src/components/library/inputs/TerritoryInput.svelte @@ -16,7 +16,6 @@ value?: string; placeholder?: string; helperText?: string; - name?: string; error?: string; onInput?: (territory: Territory) => void; onBlur?: () => void; @@ -27,7 +26,6 @@ value = $bindable(""), placeholder, helperText, - name = "address", error = undefined, onInput = undefined, onBlur = undefined, @@ -89,7 +87,6 @@ singleSelect clearable bind:searchValue={value} - inputName={name} searchPlaceholder={placeholder} {options} bind:selected From 87d4027a8d6b64a4fe83346a5b51e359198fafcd Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 24 Aug 2026 15:24:18 +0200 Subject: [PATCH 11/26] feat: recover /create/project --- .../create/CreateProjectForm.svelte | 371 ++---------------- src/i18n/locales/es.json | 27 +- src/pages/[...locale]/create/project.astro | 2 +- 3 files changed, 39 insertions(+), 361 deletions(-) diff --git a/src/components/create/CreateProjectForm.svelte b/src/components/create/CreateProjectForm.svelte index c67f15a6..33cb3b9f 100644 --- a/src/components/create/CreateProjectForm.svelte +++ b/src/components/create/CreateProjectForm.svelte @@ -7,244 +7,29 @@ apiProjectsIdPatch, type Category, type ProjectProjectCreationDto, + type ApiProjectsPostData, } from "../../openapi/client"; import { client } from "../../openapi/client/client.gen"; import { apiCategoriesIdOrSlugGetUrl } from "../../openapi/client/operation-paths.gen"; - import { - validateCreateForm, - validateField, - type ValidationErrors, - } from "../../stores/drafts/draftValidation"; - import { - createDraft, - currentDraft, - markFieldAsTouched, - touchedFields, - updateProject, - isCreateFormValid, - project, - validationErrors, - type CreateProjectForm, - } from "../../stores/drafts/projectDraft"; import { maxEndDate } from "../../utils/campaign"; import { getDefaultCurrency } from "../../utils/consts"; import { formatCurrency } from "../../utils/currencies"; import Button from "../library/buttons/Button.svelte"; import BaseCard from "../library/cards/BaseCard.svelte"; - import AddressAutocomplete from "../library/inputs/AddressAutocomplete.svelte"; import CategorySelect from "../library/inputs/CategorySelect.svelte"; import DateInput from "../library/inputs/DateInput.svelte"; import TextInput from "../library/inputs/TextInput.svelte"; import Title from "../library/typography/Title.svelte"; - - let releaseDate = $state($project.release ? new Date($project.release) : new Date()); + import TextArea from "../library/inputs/TextArea.svelte"; + import TerritoryInput from "../library/inputs/TerritoryInput.svelte"; + import { twJoin } from "tailwind-merge"; let { categories }: { categories: Category[] } = $props(); - // Track if form has been submitted once (for showing all errors) - let submitted = $state(false); - - // Loading state for form submission - let isSubmitting = $state(false); - - // API error message - let apiError: string | null = $state(null); - - // Success state - let submitSuccess = $state(false); - - // Debounce timer for real-time validation - let debounceTimer: ReturnType | null = null; - - function handleFieldBlur(fieldName: keyof CreateProjectForm) { - if (!$currentDraft) return; - - markFieldAsTouched(fieldName); - const error = validateField(fieldName, $currentDraft.createProject[fieldName]); - - validationErrors.update((errors: ValidationErrors) => { - if (error) { - return { - ...errors, - [fieldName]: error, - }; - } - - const { [fieldName]: _, ...rest } = errors; - - return rest; - }); - } - - /** - * Handles field value changes with type-safe updates. - * Uses generic typing to ensure type safety without bypassing TypeScript checks. - */ - function handleFieldChange( - fieldName: K, - value: CreateProjectForm[K], - ) { - // Update the draft value with proper typing - updateProject({ [fieldName]: value }); - - // Only validate on change if field has been touched - if ($touchedFields.has(fieldName) || submitted) { - // Debounce validation - if (debounceTimer) clearTimeout(debounceTimer); - debounceTimer = setTimeout(() => { - const error = validateField(fieldName, value); - - validationErrors.update((errors: ValidationErrors) => { - if (error) { - return { - ...errors, - [fieldName]: error, - }; - } - - const { [fieldName]: _, ...rest } = errors; - - return rest; - }); - }, 300); - } - } - - // Cleanup debounce timer on component unmount to prevent memory leaks - onDestroy(() => { - if (debounceTimer) { - clearTimeout(debounceTimer); - debounceTimer = null; - } - }); - - function handleCategoryChange(selected: Category[]) { - const categoryIris = selected.map((s) => { - return client.buildUrl({ url: apiCategoriesIdOrSlugGetUrl, path: { idOrSlug: s.id } }); - }); - - handleFieldChange("categories", categoryIris); - } - - async function handleSubmit() { - submitted = true; - apiError = null; - - const errors = validateCreateForm($project); - - // Validate entire form - const isValid = Object.keys(errors).length === 0; - - if (!isValid) { - // Scroll to error summary - const errorSummary = document.querySelector('[role="alert"]'); - if (errorSummary) { - errorSummary.scrollIntoView({ behavior: "smooth", block: "start" }); - } else { - // If no error summary, focus first invalid field - const firstError = Object.keys($validationErrors)[0]; - if (firstError) { - const element = document.querySelector(`[name="${firstError}"]`); - if (element instanceof HTMLElement) { - element.focus(); - } - } - } - return; - } - - if (!$currentDraft) { - apiError = "Draft not initialized"; - return; - } - - // Submit to API - isSubmitting = true; - - try { - const { data, error } = await apiProjectsPost({ - baseUrl: "/api/relay", - body: { - title: $currentDraft.createProject.title, - subtitle: $currentDraft.createProject.subtitle, - categories: $currentDraft.createProject.categories, - calendar: { release: $currentDraft.createProject.release ?? null }, - }, - }); - - if (error) { - if ("violations" in error && error.violations) { - error.violations.forEach((violation) => { - const field = violation.propertyPath as keyof ProjectProjectCreationDto; - if (field) { - $validationErrors[field] = violation.message || "Invalid value"; - } - }); - return; - } else { - apiError = "An unexpected error occurred. Please try again."; - return; - } - } else if (data) { - const address = $currentDraft.createProject.address; - const territory = $currentDraft.createProject.territory; - - if (address && territory) { - await apiProjectsIdPatch({ - baseUrl: "/api/relay", - path: { id: String(data.id) }, - body: { - territory: { - ...territory, - address, - }, - }, - }); - } - - submitSuccess = true; - updateProject({ - title: $currentDraft.createProject.title, - subtitle: $currentDraft.createProject.subtitle, - categories: $currentDraft.createProject.categories, - release: $currentDraft.createProject.release, - address: $currentDraft.createProject.address, - territory: $currentDraft.createProject.territory, - }); - currentDraft.update((d) => (d ? { ...d, status: "project-created" } : d)); - - // Redirect to project page - window.location.href = `/project/${data.id}/edit`; - } - } catch { - // Handle unexpected errors - apiError = "An unexpected error occurred. Please try again."; - } finally { - isSubmitting = false; - } - } - - // Helper to check if field should show error - function shouldShowError(fieldName: string): boolean { - return (submitted || $touchedFields.has(fieldName)) && !!$validationErrors[fieldName]; - } - - // Calculate minimum date (14 days from now) for date input - function getMinDate(): Date { - const minDate = new Date(); - minDate.setDate(minDate.getDate() + 14); - return minDate; - } - - $effect(() => { - if (!$currentDraft) { - createDraft({ - title: "", - subtitle: "", - categories: [], - release: undefined, - }); - } + let form = $state({ + title: "", + subtitle: "", + categories: [], }); @@ -258,77 +43,25 @@ {$t("pages.project.create.subtitle")}

- {#if submitted && Object.keys($validationErrors).length > 0} - - {/if} +
{$t("pages.project.create.description.title")}

- {$t("pages.project.create.description.subtitle")} + {$t("pages.project.create.description.description")}

handleFieldBlur("title")} - onInput={(e) => handleFieldChange("title", (e.target as HTMLInputElement).value)} + bind:value={form.title} + helperText={$t("pages.project.create.description.titlePrompt")} + placeholder={$t("pages.project.create.description.titlePlaceholder")} />
- - {#if shouldShowError("subtitle")} - - {/if} +