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 @@
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