diff --git a/frontend/messages/en.json b/frontend/messages/en.json index df3cdd3..747094b 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -1,58 +1,62 @@ { - "$schema": "https://inlang.com/schema/inlang-message-format", - "accommodation_info": "Accommodation info", - "about": "About", - "agenda": "Agenda", - "an_error_has_occurred": "An error has occurred", - "cancel": "Cancel", - "check_your_email": "Check your email", - "close": "Close", - "code_of_conduct": "Code of Conduct", - "contact_info": "Contact info", - "cookie_policy": "Cookie policy", - "day": "Day", - "did_you_forget_password": "Did you forget your password?", - "email": "Email", - "email_or_password_incorrect": "Email or password incorrect", - "edit": "Edit", - "edit_pseudonym": "Tell us your pseudonym in STE", - "edit_menu_options": "Tell us if you have any requirements on the menu (allergies, intolerances..).", - "enter_your_email": "Enter your email", - "enter_your_email_and_password": "You need to enter your email and password to see your registration data", - "enter_your_email_to_send_you_link": "Enter your email and we'll send you a link to set a new password.", - "enter_your_password": "Enter your password", - "enter_your_password_again": "Enter it again, for security", - "enter_your_new_password": "Enter your new password to access the web:", - "error_404_not_found": "404 Not found", - "error_404_message": "Sorry, we can't find the requested page. Please go back and try a different thing.", - "error_500_internal_error": "500 Internal error", - "error_500_message": "Sorry, an error has ocurred. Please try again.", - "error_generic": "Error {status}", - "forgot_password": "Forgotten password", - "forgot_password_press_here": "Forgotten password? Press here.", - "get_magic_link": "Get a magic link for another email→", - "it_has_a_magic_link": "It has a magic link to log in without a password.", - "invalid_reset_link": "The reset password link is invalid or timed out.", - "legal": "Legal", - "login": "Login", - "logout": "Logout", - "new_password": "New password", - "new_password_set": "Your new password has been set. You can now log in.", - "org_team": "Org team", - "password": "Password", - "password_length": "Password length must be at least 6 characters", - "passwords_must_match": "Passwords must match", - "press_kit": "Press kit", - "price": "Price", - "prices": "Prices", - "privacy_policy": "Privacy policy", - "register": "Register", - "registration": "Registration", - "reset_password_sent": "If your email is registered, a message has been sent with a link to set a new password.", - "send": "Send", - "speakers": "Speakers", - "streaming": "Streaming", - "venue_info": "Venue info", - "we_sent_an_email": "We sent an email to you at {email}", - "you_need_to_register": "You need to register to be able to enter the tickets queue and get your tickets." -} \ No newline at end of file + "$schema": "https://inlang.com/schema/inlang-message-format", + "accommodation_info": "Accommodation info", + "about": "About", + "agenda": "Agenda", + "an_error_has_occurred": "An error has occurred", + "cancel": "Cancel", + "check_your_email": "Check your email", + "close": "Close", + "code_of_conduct": "Code of Conduct", + "contact_info": "Contact info", + "cookie_policy": "Cookie policy", + "day": "Day", + "did_you_forget_password": "Did you forget your password?", + "email": "Email", + "email_or_password_incorrect": "Email or password incorrect", + "edit": "Edit", + "edit_pseudonym": "Tell us your pseudonym in STE", + "edit_menu_options": "Tell us if you have any requirements on the menu (allergies, intolerances..).", + "enter_your_email": "Enter your email", + "enter_your_email_and_password": "You need to enter your email and password to see your registration data", + "enter_your_email_to_send_you_link": "Enter your email and we'll send you a link to set a new password.", + "enter_your_password": "Enter your password", + "enter_your_password_again": "Enter it again, for security", + "enter_your_new_password": "Enter your new password to access the web:", + "error_403_forbidden": "Access not allowed", + "error_403_message": "You do not have permission to access this page or resource.", + "error_404_not_found": "404 Not found", + "error_404_message": "Sorry, we can't find the requested page. Please go back and try a different thing.", + "error_500_internal_error": "500 Internal error", + "error_500_message": "Sorry, an error has ocurred. Please try again.", + "error_generic": "Error {status}", + "forgot_password": "Forgotten password", + "forgot_password_forbidden": "We couldn’t send the recovery link. Please check that the email is registered or contact the organization.", + "forgot_password_press_here": "Forgotten password? Press here.", + "get_magic_link": "Get a magic link for another email→", + "it_has_a_magic_link": "It has a magic link to log in without a password.", + "invalid_reset_link": "The reset password link is invalid or timed out.", + "legal": "Legal", + "login": "Login", + "login_forbidden": "We couldn’t sign you in. Please check your password and make sure your account is active.", + "logout": "Logout", + "new_password": "New password", + "new_password_set": "Your new password has been set. You can now log in.", + "org_team": "Org team", + "password": "Password", + "password_length": "Password length must be at least 6 characters", + "passwords_must_match": "Passwords must match", + "press_kit": "Press kit", + "price": "Price", + "prices": "Prices", + "privacy_policy": "Privacy policy", + "register": "Register", + "registration": "Registration", + "reset_password_sent": "If your email is registered, a message has been sent with a link to set a new password.", + "send": "Send", + "speakers": "Speakers", + "streaming": "Streaming", + "venue_info": "Venue info", + "we_sent_an_email": "We sent an email to you at {email}", + "you_need_to_register": "You need to register to be able to enter the tickets queue and get your tickets." +} diff --git a/frontend/messages/es.json b/frontend/messages/es.json index c0bab22..4737596 100644 --- a/frontend/messages/es.json +++ b/frontend/messages/es.json @@ -24,18 +24,22 @@ "enter_your_password": "Introduce tu contraseña", "enter_your_password_again": "Introducela otra vez, por seguridad", "enter_your_new_password": "Introduce tu nueva contraseña para acceder a la web:", - "error_404_not_found": "404 Página no encontrada", + "error_403_forbidden": "Acceso no permitido", + "error_403_message": "No tienes permiso para acceder a esta página o recurso.", + "error_404_not_found": "Página no encontrada", "error_404_message": "Lo sentimos, no podemos encontrar la página solicitada. Por favor vuelve atrás e intenta algo diferente.", - "error_500_internal_error": "500 Error interno", + "error_500_internal_error": "Error interno", "error_500_message": "Lo sentimos, ha ocurrido un error. Por favor intenta de nuevo.", "error_generic": "Error {status}", "forgot_password": "Contraseña olvidada", + "forgot_password_forbidden": "No se ha podido enviar el enlace de recuperación. Revisa que el email esté registrado o contacta con la organización.", "forgot_password_press_here": "¿Olvidaste la contraseña? Pulsa aquí.", "get_magic_link": "Obtener un enlace para otro correo→", "it_has_a_magic_link": "Contiene un enlace para entrar sin contraseña.", "invalid_reset_link": "El enlace de establecer contraseña es inválido o está caducado.", "legal": "Aviso legal", "login": "Entrar", + "login_forbidden": "No se ha podido iniciar sesión. Revisa tu contraseña o que tu cuenta esté activa.", "logout": "Salir", "new_password": "Nueva contraseña", "new_password_set": "Se ha establecido tu nueva contraseña. Ahora puedes entrar.", diff --git a/frontend/src/lib/ErrorView.svelte b/frontend/src/lib/ErrorView.svelte new file mode 100644 index 0000000..f579681 --- /dev/null +++ b/frontend/src/lib/ErrorView.svelte @@ -0,0 +1,198 @@ + + +
+
+
+ {#if errorImageUrl} + Oso bailando + {/if} + +

{status}

+ +
+

{title}

+

{message}

+ + Volver al inicio +
+
+
+
+ + diff --git a/frontend/src/lib/Header.svelte b/frontend/src/lib/Header.svelte index 4106056..5b2f517 100644 --- a/frontend/src/lib/Header.svelte +++ b/frontend/src/lib/Header.svelte @@ -67,6 +67,15 @@ : null ].filter(Boolean) as MenuItem[]; + const featuredMenuItems: MenuItem[] = [ + data.show_speakers + ? { + label: m.speakers(), + href: i18n.resolveRoute('/speakers') + } + : null + ].filter(Boolean) as MenuItem[]; + const secondaryMenuItems: MenuItem[] = [ data.show_agenda ? { @@ -74,12 +83,6 @@ href: i18n.resolveRoute('/agenda') } : null, - data.show_speakers - ? { - label: m.speakers(), - href: i18n.resolveRoute('/speakers') - } - : null, data.show_streaming ? { label: m.streaming(), @@ -95,13 +98,15 @@ ] .filter(Boolean) .sort((a, b) => a.label.localeCompare(b.label, 'es', { sensitivity: 'base' })) as MenuItem[]; - const menuItems: MenuItem[] = hasCustomNavMenu ? customMenuItems - : [...priorityMenuItems, ...secondaryMenuItems]; + : [...priorityMenuItems, ...featuredMenuItems, ...secondaryMenuItems]; + + const hasHeaderCta = isRegistrationOpen && Boolean(data.register_cta); + const shouldUseDesktopOverflow = hasHeaderCta ? menuItems.length >= 5 : menuItems.length >= 6; - const desktopPrimaryMenuItems = menuItems.length > 5 ? menuItems.slice(0, 4) : menuItems; - const desktopOverflowMenuItems = menuItems.length > 5 ? menuItems.slice(4) : []; + const desktopPrimaryMenuItems = shouldUseDesktopOverflow ? menuItems.slice(0, 4) : menuItems; + const desktopOverflowMenuItems = shouldUseDesktopOverflow ? menuItems.slice(4) : []; const showMenu = menuItems.length > 0; diff --git a/frontend/src/lib/PriceImageModal.svelte b/frontend/src/lib/PriceImageModal.svelte new file mode 100644 index 0000000..f204b83 --- /dev/null +++ b/frontend/src/lib/PriceImageModal.svelte @@ -0,0 +1,400 @@ + + +{#if isOpen} + + + {#if zoomImage} + + {/if} +{/if} + + diff --git a/frontend/src/routes/+error.svelte b/frontend/src/routes/+error.svelte index 883b5ca..2284b6f 100644 --- a/frontend/src/routes/+error.svelte +++ b/frontend/src/routes/+error.svelte @@ -1,27 +1,25 @@ - -
-
- {#if page.status === 404} -

{m.error_404_not_found()}

-
-

{m.error_404_message()}

-
- {:else if page.status === 500} -

{m.error_500_internal_error()}

-
-

{m.error_500_message()}

-
- {:else} -

{m.error_generic({status:page.status})}

- {#if page.error.message} -
-

{page.error.message}

-
- {/if} - {/if} -
-
+ diff --git a/frontend/src/routes/+layout.server.js b/frontend/src/routes/+layout.server.js index ad4c812..900f857 100644 --- a/frontend/src/routes/+layout.server.js +++ b/frontend/src/routes/+layout.server.js @@ -1,15 +1,63 @@ +import { redirect } from '@sveltejs/kit'; import { fetchSingle } from '../services/api'; import { getUserCard } from '../services/users'; -export async function load({cookies}) { - let data = {} +const ROUTE_VISIBILITY_FLAGS = { + // Añade aquí las páginas que quieras bloquear cuando su flag esté en false. + // Formato: + // '/ruta-en-espanol': 'nombre_del_flag_en_strapi', + // '/internal-route': 'nombre_del_flag_en_strapi', + + '/registro': 'loginEnabled', + '/register': 'loginEnabled', + + '/noticias': 'show_news_items', + '/news-items': 'show_news_items', + + '/faqs': 'show_faqs', + + '/prensa': 'show_press_kit', + '/press-kit': 'show_press_kit', + + '/patrocinadores': 'show_sponsors_info', + '/sponsors': 'show_sponsors_info', + + '/streaming': 'show_streaming', + + '/seo': 'show_seo', + + '/tracks': 'show_tracks' +}; + +function getVisibilityFlag(pathname) { + if (ROUTE_VISIBILITY_FLAGS[pathname]) { + return ROUTE_VISIBILITY_FLAGS[pathname]; + } + + const firstSegment = `/${pathname.split('/').filter(Boolean)[0] ?? ''}`; + + return ROUTE_VISIBILITY_FLAGS[firstSegment]; +} + +export async function load({ cookies, url }) { + let data = {}; + const [settingsEntry, siteEntry] = await Promise.all([ - fetchSingle("/setting?populate=*", cookies), - fetchSingle("/site", cookies) - ]); - const userCard = getUserCard(cookies); - if (settingsEntry || siteEntry) { - data = {...settingsEntry, ...siteEntry, ...userCard} - } - return data; + fetchSingle('/setting?populate=*', cookies), + fetchSingle('/site', cookies) + ]); + + if (settingsEntry || siteEntry) { + data = { ...settingsEntry, ...siteEntry }; + } + + const visibilityFlag = getVisibilityFlag(url.pathname); + + if (visibilityFlag && data[visibilityFlag] !== true) { + redirect(302, '/no-disponible'); + } + + const userCard = getUserCard(cookies); + + return { ...data, ...userCard }; } diff --git a/frontend/src/routes/+page.js b/frontend/src/routes/+page.js index 6c98c14..f508720 100644 --- a/frontend/src/routes/+page.js +++ b/frontend/src/routes/+page.js @@ -1,4 +1,4 @@ -import { encodeQuery, fetchSingle } from '../services/api'; +import { encodeQuery, fetchBasic, fetchSingle } from '../services/api'; const homepagePath = `/homepage?${encodeQuery({ populate: '*' @@ -20,22 +20,49 @@ const registrationInfoPath = `/registration-info?${encodeQuery({ populate: '*' })}`; +const wheelGameImagePath = `/upload/files?${encodeQuery({ + filters: { + name: { + $eq: 'gira-el-timon.jpg' + } + } +})}`; + +const wheelGamePagePath = `/simple-pages?${encodeQuery({ + filters: { + path: { + $eq: 'gira-el-timon' + } + } +})}`; + /** @type {import('./$types').PageLoad} */ export async function load({ params }) { let data = {}; - const [homepageData, speakersData, siteData, registrationInfoData] = await Promise.all([ + const [ + homepageData, + speakersData, + siteData, + registrationInfoData, + wheelGameImages, + wheelGamePages + ] = await Promise.all([ fetchSingle(homepagePath), fetchSingle(speakersPath), fetchSingle(sitePath), - fetchSingle(registrationInfoPath) + fetchSingle(registrationInfoPath), + fetchBasic(wheelGameImagePath), + fetchSingle(wheelGamePagePath) ]); data = { homepage: homepageData, speakers: speakersData, site: siteData, - registrationInfo: registrationInfoData + registrationInfo: registrationInfoData, + wheelGameImage: wheelGameImages?.[0] ?? null, + wheelGamePage: wheelGamePages?.[0] ?? null }; return data; diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index 1efd26f..1598fd9 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -131,9 +131,35 @@ FINISHED: 'finished' }; - const eventStartDate = new Date('2026-09-24T00:00:00'); - const today = new Date(); - const daysUntilEvent = Math.max(0, Math.ceil((eventStartDate - today) / (1000 * 60 * 60 * 24))); + const MS_PER_DAY = 1000 * 60 * 60 * 24; + + function localDate(year, month, day) { + return new Date(year, month - 1, day); + } + + function startOfLocalDay(date) { + return new Date(date.getFullYear(), date.getMonth(), date.getDate()); + } + + const eventStartDate = localDate(2026, 9, 24); + const eventEndDate = localDate(2026, 9, 27); + const postEventDate = localDate(2026, 9, 28); + const today = startOfLocalDay(new Date()); + // const today = localDate(2026, 9, 24); + // const today = localDate(2026, 9, 25); + // const today = localDate(2026, 9, 30); + + const daysUntilEvent = Math.max(0, Math.round((eventStartDate - today) / MS_PER_DAY)); + + let heroStatusMessage = ''; + + if (today >= postEventDate) { + heroStatusMessage = 'Nos vemos en la próxima EstelCon'; + } else if (today.getTime() === eventStartDate.getTime()) { + heroStatusMessage = '¡Hoy comienza esta travesía!'; + } else if (today > eventStartDate && today <= eventEndDate) { + heroStatusMessage = 'La Mereth Gaerlen ya está en marcha'; + } const eventStatus = site?.eventStatus ?? site?.event_status ?? EVENT_STATUS.HYPE; function plainStatusText(content, fallback) { @@ -160,6 +186,19 @@ 'Inscripciones finalizadas' ); const heroImage = heroLosPuertosGrises; + + function getStrapiMediaUrl(image) { + if (!image?.url) return ''; + + if (image.url.startsWith('http')) { + return image.url; + } + + return `${storage_url}${image.url}`; + } + + const wheelGameImageUrl = getStrapiMediaUrl(data.wheelGameImage); + const showWheelGame = Boolean(wheelGameImageUrl && data.wheelGamePage);
@@ -219,11 +258,17 @@ -
- Faltan - {daysUntilEvent} - días -
+ {#if heroStatusMessage} +
+

{heroStatusMessage}

+
+ {:else} +
+ Faltan + {daysUntilEvent} + días +
+ {/if} @@ -255,36 +300,57 @@
-
+
📜

Conferencias

Encuentros, conversaciones y reflexiones para compartir la obra de Tolkien desde distintas miradas.

-
+
-
+
🪶

Talleres

Actividades participativas para crear, aprender y disfrutar en comunidad durante el evento.

-
+ -
+
🎶

Conciertos y lecturas

Música, relatos y momentos compartidos en un ambiente cuidado junto al mar.

-
+ -
+

Más actividades

Artesanía, juegos y propuestas para vivir la Estelcon durante todo el día.

-
+ + + {#if showWheelGame} + + {/if}
@@ -519,23 +585,21 @@ {/if} diff --git a/frontend/src/routes/error/+page.svelte b/frontend/src/routes/error/+page.svelte new file mode 100644 index 0000000..8d97e9b --- /dev/null +++ b/frontend/src/routes/error/+page.svelte @@ -0,0 +1,30 @@ + + + diff --git a/frontend/src/routes/forgot-password/+page.server.js b/frontend/src/routes/forgot-password/+page.server.js index 412916d..c9ffec9 100644 --- a/frontend/src/routes/forgot-password/+page.server.js +++ b/frontend/src/routes/forgot-password/+page.server.js @@ -1,51 +1,56 @@ import { fail, redirect } from '@sveltejs/kit'; import { message, superValidate } from 'sveltekit-superforms/server'; import { z } from 'zod'; -import * as m from '$lib/paraglide/messages.js' +import * as m from '$lib/paraglide/messages.js'; import { zod } from 'sveltekit-superforms/adapters'; import { fetchCMSData } from '../../services/api'; import { isAuthorizedUser, setUser } from '../../services/users'; -let data = {} +let data = {}; const schema = z.object({ - email: z.string().email(), + email: z.string().email() }); export async function load({ params, cookies }) { - if (isAuthorizedUser(cookies)) { - redirect(302, "/me"); - } - const form = await superValidate(zod(schema)); - return { form }; + if (isAuthorizedUser(cookies)) { + redirect(302, '/me'); + } + const form = await superValidate(zod(schema)); + return { form }; } export const actions = { - default: async ({ request, cookies }) => { - const form = await superValidate(request, zod(schema)); - - if (!form.valid) { - return fail(400, { form }); - } - - const { response, error } = await fetchCMSData( - "POST", "/auth/forgot-password", { - email: form.data.email, - }, cookies - ) - - if (response) { - return message(form, {success: m.reset_password_sent()}) - } - - let error_msg; - if (error?.status === 400) { - error_msg = m.email_or_password_incorrect(); - } else if (error?.message) { - error_msg = error.message; - } else { - error_msg = m.an_error_has_occurred(); - } - return message(form, {error: error_msg}); - } - }; \ No newline at end of file + default: async ({ request, cookies }) => { + const form = await superValidate(request, zod(schema)); + + if (!form.valid) { + return fail(400, { form }); + } + + const { response, error } = await fetchCMSData( + 'POST', + '/auth/forgot-password', + { + email: form.data.email + }, + cookies + ); + + if (response) { + return message(form, { success: m.reset_password_sent() }); + } + + let error_msg; + if (error?.status === 400) { + error_msg = m.email_or_password_incorrect(); + } else if (error?.status === 403) { + error_msg = m.forgot_password_forbidden(); + } else if (error?.message) { + error_msg = error.message; + } else { + error_msg = m.an_error_has_occurred(); + } + return message(form, { error: error_msg }); + } +}; diff --git a/frontend/src/routes/gira-el-timon/+page.svelte b/frontend/src/routes/gira-el-timon/+page.svelte index 9132510..4318e6b 100644 --- a/frontend/src/routes/gira-el-timon/+page.svelte +++ b/frontend/src/routes/gira-el-timon/+page.svelte @@ -100,14 +100,16 @@ {/if}
- +
+ +
@@ -161,16 +163,27 @@ .wheel-game { display: grid; justify-items: center; - gap: 2rem; - padding: 1rem 0 2rem; + gap: 3.25rem; + padding: 1rem 0 3.5rem; + } + + .wheel-actions { + position: relative; + z-index: 20; + display: flex; + justify-content: center; + width: 100%; } .wheel-spin-button { + position: relative; + z-index: 21; padding: 1rem 2rem; background: #d7b56d; color: #0d3b44; font-family: var(--bulma-family-primary, inherit); font-weight: 800; + line-height: 1.15; box-shadow: 0 0.8rem 1.8rem rgba(13, 59, 68, 0.18); } @@ -184,10 +197,10 @@ --number-radius: 11.5rem; position: relative; - width: min(100%, 34rem); - aspect-ratio: 1; display: grid; place-items: center; + width: min(100%, 34rem); + aspect-ratio: 1; } .wheel-pointer { @@ -257,12 +270,10 @@ position: absolute; top: 50%; left: 50%; - transform: translate(-50%, -50%) rotate(var(--angle)) - translateY(calc(-1 * var(--number-radius))) rotate(calc(-1 * var(--angle))); - width: 3rem; - height: 3rem; display: grid; place-items: center; + width: 3rem; + height: 3rem; border: 2px solid #0d3b44; border-radius: 50%; background: rgba(255, 244, 207, 0.96); @@ -270,6 +281,8 @@ font-family: var(--bulma-family-primary, inherit); font-weight: 800; box-shadow: 0 0.45rem 0.9rem rgba(13, 59, 68, 0.16); + transform: translate(-50%, -50%) rotate(var(--angle)) + translateY(calc(-1 * var(--number-radius))) rotate(calc(-1 * var(--angle))); } .wheel__number span { @@ -289,8 +302,8 @@ .prize-modal__backdrop { position: absolute; inset: 0; - background: rgba(0, 0, 0, 0.55); border: 0; + background: rgba(0, 0, 0, 0.55); } .prize-card { @@ -298,20 +311,20 @@ z-index: 1; width: min(100%, 34rem); padding: 2rem; - background: #fff4cf; border: 3px solid #4d2b13; + background: #fff4cf; + text-align: center; box-shadow: inset 0 0 0 4px rgba(255, 255, 255, 0.38), 0 1.4rem 3rem rgba(0, 0, 0, 0.32); - text-align: center; } .prize-card__eyebrow { margin-bottom: 0.4rem; + color: #b06b2d; font-family: var(--bulma-family-primary, inherit); font-weight: 800; text-transform: uppercase; - color: #b06b2d; } .prize-card__title { @@ -330,15 +343,56 @@ } @media screen and (max-width: 768px) { + .wheel-game { + gap: 3rem; + padding-bottom: 4rem; + } + .wheel-stage { --number-radius: 7.4rem; + width: min(100%, 22rem); } + .wheel-spin-button { + width: min(100%, 18rem); + white-space: normal; + } + .wheel__number { width: 2.25rem; height: 2.25rem; font-size: 0.9rem; } } + + @media screen and (max-width: 420px) { + .wheel-stage { + --number-radius: 6.55rem; + } + + .wheel-pointer { + top: -2.15rem; + width: 3rem; + height: 3.9rem; + } + + .wheel-pointer::before { + width: 1.15rem; + height: 1.15rem; + border-width: 2px; + } + + .wheel-pointer::after { + top: 0.85rem; + width: 1.7rem; + height: 2.8rem; + } + + .wheel__number { + width: 2rem; + height: 2rem; + font-size: 0.82rem; + } + } diff --git a/frontend/src/routes/login/+page.server.js b/frontend/src/routes/login/+page.server.js index a506c3b..5d3d7a1 100644 --- a/frontend/src/routes/login/+page.server.js +++ b/frontend/src/routes/login/+page.server.js @@ -1,54 +1,59 @@ import { fail, redirect } from '@sveltejs/kit'; import { message, superValidate } from 'sveltekit-superforms/server'; import { z } from 'zod'; -import * as m from '$lib/paraglide/messages.js' +import * as m from '$lib/paraglide/messages.js'; import { zod } from 'sveltekit-superforms/adapters'; import { fetchCMSData } from '../../services/api'; import { isAuthorizedUser, setUser } from '../../services/users'; -let data = {} +let data = {}; const schema = z.object({ - email: z.string().email(), - password: z.string(), + email: z.string().email(), + password: z.string() }); export async function load({ params, cookies }) { - if (isAuthorizedUser(cookies)) { - redirect(302, "/me"); - } - const form = await superValidate(zod(schema)); - return { form }; + if (isAuthorizedUser(cookies)) { + redirect(302, '/me'); + } + const form = await superValidate(zod(schema)); + return { form }; } export const actions = { - default: async ({ request, cookies }) => { - const form = await superValidate(request, zod(schema)); - - if (!form.valid) { - return fail(400, { form }); - } - - const { response, error } = await fetchCMSData( - "POST", "/auth/local", { - identifier: form.data.email, - password: form.data.password - }, cookies - ) - - if (response) { - setUser(cookies, response.jwt, response.user); - redirect(302, "/me") - } - - let error_msg; - if (error?.status === 400) { - error_msg = m.email_or_password_incorrect(); - } else if (error?.message) { - error_msg = error.message; - } else { - error_msg = m.an_error_has_occurred(); - } - return message(form, error_msg); - } - }; \ No newline at end of file + default: async ({ request, cookies }) => { + const form = await superValidate(request, zod(schema)); + + if (!form.valid) { + return fail(400, { form }); + } + + const { response, error } = await fetchCMSData( + 'POST', + '/auth/local', + { + identifier: form.data.email, + password: form.data.password + }, + cookies + ); + + if (response) { + setUser(cookies, response.jwt, response.user); + redirect(302, '/me'); + } + + let error_msg; + if (error?.status === 400) { + error_msg = m.email_or_password_incorrect(); + } else if (error?.status === 403) { + error_msg = m.login_forbidden(); + } else if (error?.message) { + error_msg = error.message; + } else { + error_msg = m.an_error_has_occurred(); + } + return message(form, error_msg); + } +}; diff --git a/frontend/src/routes/price/+page.js b/frontend/src/routes/price/+page.js index f6c0294..cf72455 100644 --- a/frontend/src/routes/price/+page.js +++ b/frontend/src/routes/price/+page.js @@ -2,30 +2,32 @@ import { encodeQuery, fetchBasic, fetchCollection } from '../../services/api'; /** @type {import('./$types').PageLoad} */ export async function load({}) { - const tshirtFrontImagePath = `/upload/files?${encodeQuery({ + const tshirtImagesPath = `/upload/files?${encodeQuery({ filters: { name: { - $eq: 'camiseta.jpg' + $startsWith: 'camiseta' } - } + }, + sort: ['name:asc'] })}`; - const tshirtBackImagePath = `/upload/files?${encodeQuery({ + const allIncludedPriceCardPath = `/upload/files?${encodeQuery({ filters: { name: { - $eq: 'camiseta_trasera.jpg' + $eq: 'carta_ti.jpeg' } } })}`; - const [priceEntries, tshirtFrontImages, tshirtBackImages] = await Promise.all([ + const [priceEntries, tshirtImages, allIncludedPriceCards] = await Promise.all([ fetchCollection('/prices?populate=*'), - fetchBasic(tshirtFrontImagePath), - fetchBasic(tshirtBackImagePath) + fetchBasic(tshirtImagesPath), + fetchBasic(allIncludedPriceCardPath) ]); return { prices: priceEntries ?? [], - tshirtImages: [tshirtFrontImages?.[0], tshirtBackImages?.[0]].filter(Boolean) + tshirtImages: tshirtImages ?? [], + allIncludedPriceCard: allIncludedPriceCards?.[0] ?? null }; } diff --git a/frontend/src/routes/price/+page.svelte b/frontend/src/routes/price/+page.svelte index d079334..c3b7868 100644 --- a/frontend/src/routes/price/+page.svelte +++ b/frontend/src/routes/price/+page.svelte @@ -2,7 +2,7 @@ import SvelteMarkdown from '@humanspeak/svelte-markdown'; import { modals } from 'svelte-modals'; import * as m from '$lib/paraglide/messages.js'; - import ActivityModal from '$lib/ActivityModal.svelte'; + import PriceImageModal from '$lib/PriceImageModal.svelte'; import roomImage from '../../assets/images/camarote.jpeg'; import treasureImage from '../../assets/images/tesoro.jpeg'; @@ -26,6 +26,7 @@ prices: Price[]; content?: string; tshirtImages?: StrapiImage[]; + allIncludedPriceCard?: StrapiImage | null; } export let data: Data; @@ -60,6 +61,15 @@ return price.name.toLowerCase().includes('camiseta'); } + function isAllIncludedPrice(price: Price): boolean { + const normalizedName = price.name + .toLowerCase() + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, ''); + + return normalizedName.includes('todo incluido'); + } + function getStrapiMediaUrl(image?: StrapiImage | null): string { if (!image?.url) return ''; @@ -75,24 +85,36 @@ .map((image) => getStrapiMediaUrl(image)) .filter(Boolean); - modals.open(ActivityModal, { - activity: { - title: price.name, - short_description: price.description, - long_description: - tshirtImageUrls.length > 0 - ? tshirtImageUrls - .map((url, index) => { - const label = - index === 0 - ? 'Diseño delantero de la camiseta' - : 'Diseño trasero de la camiseta'; - return `![${label}](${url})`; - }) - .join(' ') - : 'El diseño de la camiseta todavía no está disponible.', - tag1: 'Diseño' - } + modals.open(PriceImageModal, { + title: price.name, + short_description: price.description, + images: tshirtImageUrls.map((url, index) => ({ + url, + alt: index === 0 ? 'Diseño delantero de la camiseta' : 'Diseño trasero de la camiseta' + })), + tag1: 'Diseño', + imageZoom: true, + unavailableText: 'El diseño de la camiseta todavía no está disponible.' + }); + } + + function openAllIncludedModal(price: Price) { + const priceCardUrl = getStrapiMediaUrl(data.allIncludedPriceCard); + + modals.open(PriceImageModal, { + title: price.name, + short_description: price.description, + images: priceCardUrl + ? [ + { + url: priceCardUrl, + alt: 'Carta de precios todo incluido' + } + ] + : [], + tag1: 'Carta de precios', + imageZoom: true, + unavailableText: 'La carta de precios todavía no está disponible.' }); } @@ -219,6 +241,32 @@ {/if}
+
+ {#if isPricePending(priceObj.value)} + PRECIO POR DETERMINAR + {:else} + {priceObj.value} + {priceObj.currency} + {/if} +
+ + {:else if isAllIncludedPrice(priceObj)} +