diff --git a/src/components/admin/comm/banners/BannersForm.svelte b/src/components/admin/comm/banners/BannersForm.svelte index 365ded06..4aabdcb2 100644 --- a/src/components/admin/comm/banners/BannersForm.svelte +++ b/src/components/admin/comm/banners/BannersForm.svelte @@ -2,7 +2,7 @@ import { actions } from "astro:actions"; import { locale, t } from "../../../../i18n/store"; - import { formatDate } from "../../../../utils/dates"; + import { endOfDay, formatDate, startOfDay } from "../../../../utils/dates"; import ActionableButton from "../../../library/buttons/ActionableButton.svelte"; import DateInput from "../../../library/inputs/DateInput.svelte"; import TextArea from "../../../library/inputs/TextArea.svelte"; @@ -23,6 +23,32 @@ let fieldErrors: FieldErrors = $state({}); + let startsAt = $state(new Date()); + let endsAt = $state(new Date()); + + /** + * Normalize a date field to the human expected true datetime + * @param field + * @param date + */ + function normalizeDate(field: "startsAt" | "endsAt", date: Date): Date { + const now = new Date(); + const isToday = + date.getFullYear() === now.getFullYear() && + date.getMonth() === now.getMonth() && + date.getDate() === now.getDate(); + + // Ends always run to the end of their day. Start dates run from their + // midnight, except when the day is today the current time is used so it + // passes the "no earlier than now" validation. DateInput binds the same + // object, so the normalized time is applied in place. + const normalized = field === "endsAt" ? endOfDay(date) : isToday ? now : startOfDay(date); + + date.setTime(normalized.getTime()); + + return date; + } + async function submit() { fieldErrors = {}; @@ -100,19 +126,23 @@
(startsAt = normalizeDate("startsAt", date))} /> (endsAt = normalizeDate("endsAt", date))} />
diff --git a/src/components/home/HomeBanner.svelte b/src/components/home/HomeBanner.svelte index e0e4ecf4..a80d7b3d 100644 --- a/src/components/home/HomeBanner.svelte +++ b/src/components/home/HomeBanner.svelte @@ -18,7 +18,7 @@ function handleClose() { visible = false; - if (onClose) onClose(); + onClose?.(); } @@ -67,7 +67,7 @@ diff --git a/src/components/home/HomeBanners.svelte b/src/components/home/HomeBanners.svelte new file mode 100644 index 00000000..d34d0d2a --- /dev/null +++ b/src/components/home/HomeBanners.svelte @@ -0,0 +1,55 @@ + + +{#if visibleBanners.length > 0} +
+ {#each visibleBanners as banner (banner.id)} + handleClose(banner.id)} + /> + {/each} +
+{/if} diff --git a/src/i18n/locales/ca.json b/src/i18n/locales/ca.json index 2e088f25..011b6ee0 100644 --- a/src/i18n/locales/ca.json +++ b/src/i18n/locales/ca.json @@ -1775,6 +1775,10 @@ "modal": { "title": "Anunci publicat el {{date}}", "description": "Aquí pots veure com era l'anunci" + }, + "deleteModal": { + "title": "Voleu suprimir aquest anunci?", + "description": "L'anunci se suprimirà permanentment i desapareixerà de l'historial. Aquesta acció no es pot desfer." } } } diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 6ccbfebd..835bdbe5 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -1775,6 +1775,10 @@ "modal": { "title": "Announcement published on {{date}}", "description": "Here you can see what the announcement looked like" + }, + "deleteModal": { + "title": "Delete this announcement?", + "description": "The announcement will be permanently deleted and disappear from the history. This action cannot be undone." } } } diff --git a/src/i18n/locales/es.json b/src/i18n/locales/es.json index 05302b42..4e785eba 100644 --- a/src/i18n/locales/es.json +++ b/src/i18n/locales/es.json @@ -27,7 +27,7 @@ "expectedUrl": "No es una URL" }, "date": { - "greaterThan": "La fecha debe ser mayor que {{ date }}" + "greaterThan": "La fecha debe ser mayor que el {{ date }}" }, "file": { "maxAllowedSize": "Tamaño máximo permitido {{ size }}", diff --git a/src/pages/[...locale]/index.astro b/src/pages/[...locale]/index.astro index 7a13c1d8..2d91b411 100644 --- a/src/pages/[...locale]/index.astro +++ b/src/pages/[...locale]/index.astro @@ -8,11 +8,12 @@ import Stats from "../../components/home/Stats.astro"; import FeaturedPromoters from "../../components/home/FeaturedPromoters.astro"; import Blog from "../../components/home/Blog.astro"; import Matchfunding from "../../components/home/Matchfunding.astro"; -import HomeBanner from "../../components/home/HomeBanner.svelte"; +import Banners from "../../components/home/HomeBanners.svelte"; // Disabled sections, re-enable the import along with the component below: // import Stories from "../../components/home/Stories.svelte"; // import Resources from "../../components/home/Resources.astro"; import { isSupportedLocale } from "../../i18n/locales"; +import { bannerRepository } from "../../repositories/banners"; const { locale } = Astro.params; @@ -21,21 +22,14 @@ if (locale && !isSupportedLocale(locale)) { } const { t, lang } = Astro.locals; + +const activeBanners = await bannerRepository.getActive(); --- -
- -
+ diff --git a/src/repositories/banners.ts b/src/repositories/banners.ts index 48d17533..3d4016be 100644 --- a/src/repositories/banners.ts +++ b/src/repositories/banners.ts @@ -34,6 +34,37 @@ class BannerRepository { ORDER BY date_created DESC`, ) .all() + .then((data) => + data.results.map((r) => { + return { + ...r, + startsAt: new Date(r.startsAt), + endsAt: new Date(r.endsAt), + dateCreated: new Date(r.dateCreated), + }; + }), + ); + } + + public async getActive(): Promise { + const now = Date.now(); + return await this.db + .prepare( + `SELECT + id, + title, + content, + cta_text AS ctaText, + cta_link AS ctaLink, + starts_at AS startsAt, + ends_at AS endsAt, + date_created AS dateCreated + FROM banners + WHERE starts_at <= ? AND ends_at >= ? + ORDER BY date_created DESC`, + ) + .bind(now, now) + .all() .then((data) => data.results.map((r) => ({ ...r, @@ -44,8 +75,8 @@ class BannerRepository { ); } - public async create(banner: Omit): Promise { - await this.db + public async create(banner: Omit): Promise> { + const result = await this.db .prepare( `INSERT INTO banners (title, content, cta_text, cta_link, starts_at, ends_at, date_created) VALUES (?, ?, ?, ?, ?, ?, ?)`, @@ -60,6 +91,12 @@ class BannerRepository { banner.dateCreated.getTime(), ) .run(); + + if (result.error) { + console.error(result); + } + + return banner; } public async delete(id: number): Promise { diff --git a/src/utils/dates.ts b/src/utils/dates.ts index 7e17e017..b290015e 100644 --- a/src/utils/dates.ts +++ b/src/utils/dates.ts @@ -10,6 +10,27 @@ export function formatDate(date: Date, locale: Locale) { return date.toLocaleDateString(locale, options); } +/** + * The first instant of the day for the given date (midnight, 00:00). + * @param date The date to normalize + * @returns A new Date set to the start of the given day + */ +export function startOfDay(date: Date): Date { + return new Date(date.getFullYear(), date.getMonth(), date.getDate()); +} + +/** + * The last instant of the day for the given date (23:59:59.999). + * Derived from the start of the following day rather than hardcoding the end-of-day time. + * @param date The date to normalize + * @returns A new Date set to the end of the given day + */ +export function endOfDay(date: Date): Date { + // Start of the next day, minus a single millisecond + const nextDay = new Date(date.getFullYear(), date.getMonth(), date.getDate() + 1); + return new Date(nextDay.getTime() - 1); +} + /** * Get a date `n` ms from now * @param offset Milliseconds since now until future date