Further improvements on the open hours endpoints

This commit is contained in:
2026-02-24 21:52:16 +02:00
parent 082eb2575e
commit bc4c849590
11 changed files with 425 additions and 18 deletions

View File

@@ -2,27 +2,74 @@ public static class LokEndpoints
{ {
public static void MapLokEndpoints(WebApplication app) public static void MapLokEndpoints(WebApplication app)
{ {
app.MapPost("/lok/open-hours", async (LokOpenHours openHours, LokService lokService) => app.MapPost("/lok/open-hours", async (HttpContext httpContext) =>
{ {
var lokService = httpContext.RequestServices.GetRequiredService<LokService>();
var openHours = await httpContext.Request.ReadFromJsonAsync<LokOpenHours>();
if (openHours is null)
{
httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
await httpContext.Response.WriteAsJsonAsync(new
{
Message = "Request body is required."
});
return;
}
if (string.IsNullOrWhiteSpace(openHours.Name))
{
httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
await httpContext.Response.WriteAsJsonAsync(new
{
Message = "Open hours version name is required."
});
return;
}
var createdOpenHours = await lokService.InsertOpenHours(openHours); var createdOpenHours = await lokService.InsertOpenHours(openHours);
return Results.Created("/lok/open-hours", createdOpenHours); httpContext.Response.StatusCode = StatusCodes.Status201Created;
httpContext.Response.Headers.Location = "/lok/open-hours";
await httpContext.Response.WriteAsJsonAsync(createdOpenHours);
}) })
.WithName("CreateLokOpenHours"); .WithName("CreateLokOpenHours");
app.MapGet("/lok/open-hours", async (LokService lokService) => app.MapGet("/lok/open-hours", async (HttpContext httpContext) =>
{ {
var lokService = httpContext.RequestServices.GetRequiredService<LokService>();
var openHours = await lokService.GetOpenHours(); var openHours = await lokService.GetOpenHours();
if (openHours.Count == 0) if (openHours.Count == 0)
{ {
return Results.NotFound(new httpContext.Response.StatusCode = StatusCodes.Status404NotFound;
await httpContext.Response.WriteAsJsonAsync(new
{ {
Message = "Open hours not found." Message = "Open hours not found."
}); });
return;
} }
return Results.Ok(openHours); await httpContext.Response.WriteAsJsonAsync(openHours);
}) })
.WithName("GetLokOpenHours"); .WithName("GetLokOpenHours");
app.MapDelete("/lok/open-hours/{id:long}", async (HttpContext httpContext, long id) =>
{
var lokService = httpContext.RequestServices.GetRequiredService<LokService>();
var deleted = await lokService.DeleteOpenHours(id);
if (!deleted)
{
httpContext.Response.StatusCode = StatusCodes.Status404NotFound;
await httpContext.Response.WriteAsJsonAsync(new
{
Message = "Open hours version not found."
});
return;
}
httpContext.Response.StatusCode = StatusCodes.Status204NoContent;
})
.WithName("DeleteLokOpenHours");
} }
} }

View File

@@ -11,18 +11,18 @@ public static class SystemEndpoints
}) })
.WithName("GetVersion"); .WithName("GetVersion");
app.MapGet("/health/db", async (SqliteConnection connection) => app.MapGet("/health/db", async (Microsoft.Data.Sqlite.SqliteConnection connection) =>
{ {
await connection.OpenAsync(); await connection.OpenAsync();
await using var command = connection.CreateCommand(); await using var command = connection.CreateCommand();
command.CommandText = "SELECT 1"; command.CommandText = "SELECT 1";
var result = await command.ExecuteScalarAsync(); var result = await command.ExecuteScalarAsync();
return Results.Ok(new return new
{ {
Database = "ok", Database = "ok",
Result = result Result = result
}); };
}) })
.WithName("GetDatabaseHealth"); .WithName("GetDatabaseHealth");
} }

View File

@@ -1,5 +1,9 @@
public class LokOpenHours public class LokOpenHours
{ {
public long Id { get; set; }
public string Name { get; set; } = string.Empty;
public DateTime Version { get; set; } public DateTime Version { get; set; }
public string Paragraph1 { get; set; } = string.Empty; public string Paragraph1 { get; set; } = string.Empty;

View File

@@ -55,6 +55,15 @@ public class Program
command.CommandText = "ALTER TABLE LokOpenHours ADD COLUMN version TEXT NOT NULL DEFAULT '';"; command.CommandText = "ALTER TABLE LokOpenHours ADD COLUMN version TEXT NOT NULL DEFAULT '';";
command.ExecuteNonQuery(); command.ExecuteNonQuery();
} }
command.CommandText = "SELECT COUNT(*) FROM pragma_table_info('LokOpenHours') WHERE name = 'name';";
var hasNameColumn = Convert.ToInt32(command.ExecuteScalar()) > 0;
if (!hasNameColumn)
{
command.CommandText = "ALTER TABLE LokOpenHours ADD COLUMN name TEXT NOT NULL DEFAULT '';";
command.ExecuteNonQuery();
}
} }
} }

View File

@@ -19,7 +19,7 @@ public class LokService
await using var command = _connection.CreateCommand(); await using var command = _connection.CreateCommand();
command.CommandText = @" command.CommandText = @"
SELECT version, paragraph1, paragraph2, paragraph3, paragraph4, kitchenNotice SELECT id, name, version, paragraph1, paragraph2, paragraph3, paragraph4, kitchenNotice
FROM LokOpenHours FROM LokOpenHours
ORDER BY datetime(version) DESC, id DESC ORDER BY datetime(version) DESC, id DESC
LIMIT 5"; LIMIT 5";
@@ -32,6 +32,8 @@ public class LokService
{ {
openHoursList.Add(new LokOpenHours openHoursList.Add(new LokOpenHours
{ {
Id = reader["id"] is long id ? id : Convert.ToInt64(reader["id"]),
Name = reader["name"]?.ToString() ?? string.Empty,
Version = ParseVersion(reader["version"]?.ToString()), Version = ParseVersion(reader["version"]?.ToString()),
Paragraph1 = reader["paragraph1"]?.ToString() ?? string.Empty, Paragraph1 = reader["paragraph1"]?.ToString() ?? string.Empty,
Paragraph2 = reader["paragraph2"]?.ToString() ?? string.Empty, Paragraph2 = reader["paragraph2"]?.ToString() ?? string.Empty,
@@ -55,9 +57,11 @@ public class LokService
await using var command = _connection.CreateCommand(); await using var command = _connection.CreateCommand();
command.CommandText = @" command.CommandText = @"
INSERT INTO LokOpenHours (version, paragraph1, paragraph2, paragraph3, paragraph4, kitchenNotice) INSERT INTO LokOpenHours (name, version, paragraph1, paragraph2, paragraph3, paragraph4, kitchenNotice)
VALUES (@version, @paragraph1, @paragraph2, @paragraph3, @paragraph4, @kitchenNotice);"; VALUES (@name, @version, @paragraph1, @paragraph2, @paragraph3, @paragraph4, @kitchenNotice);
SELECT last_insert_rowid();";
command.Parameters.AddWithValue("@name", openHours.Name ?? string.Empty);
command.Parameters.AddWithValue("@version", version.ToString("O")); command.Parameters.AddWithValue("@version", version.ToString("O"));
command.Parameters.AddWithValue("@paragraph1", openHours.Paragraph1 ?? string.Empty); command.Parameters.AddWithValue("@paragraph1", openHours.Paragraph1 ?? string.Empty);
command.Parameters.AddWithValue("@paragraph2", openHours.Paragraph2 ?? string.Empty); command.Parameters.AddWithValue("@paragraph2", openHours.Paragraph2 ?? string.Empty);
@@ -65,10 +69,12 @@ public class LokService
command.Parameters.AddWithValue("@paragraph4", openHours.Paragraph4 ?? string.Empty); command.Parameters.AddWithValue("@paragraph4", openHours.Paragraph4 ?? string.Empty);
command.Parameters.AddWithValue("@kitchenNotice", openHours.KitchenNotice ?? string.Empty); command.Parameters.AddWithValue("@kitchenNotice", openHours.KitchenNotice ?? string.Empty);
await command.ExecuteNonQueryAsync(); var insertedId = await command.ExecuteScalarAsync();
return new LokOpenHours return new LokOpenHours
{ {
Id = Convert.ToInt64(insertedId),
Name = openHours.Name ?? string.Empty,
Version = version, Version = version,
Paragraph1 = openHours.Paragraph1 ?? string.Empty, Paragraph1 = openHours.Paragraph1 ?? string.Empty,
Paragraph2 = openHours.Paragraph2 ?? string.Empty, Paragraph2 = openHours.Paragraph2 ?? string.Empty,
@@ -78,6 +84,24 @@ public class LokService
}; };
} }
public async Task<bool> DeleteOpenHours(long id)
{
if (_connection.State != ConnectionState.Open)
{
await _connection.OpenAsync();
}
await using var command = _connection.CreateCommand();
command.CommandText = @"
DELETE FROM LokOpenHours
WHERE id = @id;";
command.Parameters.AddWithValue("@id", id);
var affectedRows = await command.ExecuteNonQueryAsync();
return affectedRows > 0;
}
private static DateTime ParseVersion(string? value) private static DateTime ParseVersion(string? value)
{ {
if (!string.IsNullOrWhiteSpace(value) && DateTime.TryParse(value, out var parsed)) if (!string.IsNullOrWhiteSpace(value) && DateTime.TryParse(value, out var parsed))

View File

@@ -1,5 +1,6 @@
CREATE TABLE IF NOT EXISTS LokOpenHours ( CREATE TABLE IF NOT EXISTS LokOpenHours (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL DEFAULT '',
version TEXT NOT NULL, version TEXT NOT NULL,
paragraph1 TEXT NOT NULL DEFAULT '', paragraph1 TEXT NOT NULL DEFAULT '',
paragraph2 TEXT NOT NULL DEFAULT '', paragraph2 TEXT NOT NULL DEFAULT '',

Binary file not shown.

View File

@@ -1,4 +1,4 @@
import { query } from "@solidjs/router"; import { action, query } from "@solidjs/router";
const API_BASE_URL = process.env.API_BASE_URL ?? "http://localhost:5013"; const API_BASE_URL = process.env.API_BASE_URL ?? "http://localhost:5013";
@@ -19,6 +19,10 @@ async function fetchApi<T>(path: string, init?: RequestInit): Promise<T> {
throw new Error(`API ${response.status}: ${text || response.statusText}`); throw new Error(`API ${response.status}: ${text || response.statusText}`);
} }
if (response.status === 204) {
return undefined as T;
}
return (await response.json()) as T; return (await response.json()) as T;
} }
@@ -27,3 +31,60 @@ export const queryApiVersion = query(async () => {
const data = await fetchApi<{ version: string }>("/"); const data = await fetchApi<{ version: string }>("/");
return data.version; return data.version;
}, "api-version"); }, "api-version");
export type LokOpenHours = {
id: number;
name: string;
version: string;
paragraph1: string;
paragraph2: string;
paragraph3: string;
paragraph4: string;
kitchenNotice: string;
};
export const queryLokOpenHours = query(async (_refreshKey = 0) => {
"use server";
return await fetchApi<LokOpenHours[]>("/lok/open-hours");
}, "lok-open-hours");
export const createLokOpenHours = action(async (formData: FormData) => {
"use server";
const name = String(formData.get("name") ?? "").trim();
if (!name) {
throw new Error("Open hours version name is required.");
}
const payload = {
id: 0,
name,
version: new Date().toISOString(),
paragraph1: String(formData.get("paragraph1") ?? ""),
paragraph2: String(formData.get("paragraph2") ?? ""),
paragraph3: String(formData.get("paragraph3") ?? ""),
paragraph4: String(formData.get("paragraph4") ?? ""),
kitchenNotice: String(formData.get("kitchenNotice") ?? ""),
} satisfies LokOpenHours;
return await fetchApi<LokOpenHours>("/lok/open-hours", {
method: "POST",
body: JSON.stringify(payload),
});
});
export const deleteLokOpenHours = action(async (formData: FormData) => {
"use server";
const idValue = String(formData.get("id") ?? "").trim();
const id = Number(idValue);
if (!Number.isFinite(id) || id <= 0) {
throw new Error("Open hours id is required for delete.");
}
await fetchApi<void>(`/lok/open-hours/${id}`, {
method: "DELETE",
});
return { deleted: true };
});

View File

@@ -0,0 +1,228 @@
import { createAsync, useSubmission } from "@solidjs/router";
import { For, Show, createEffect, createMemo, createSignal } from "solid-js";
import { createLokOpenHours, deleteLokOpenHours, queryLokOpenHours } from "~/api";
import { t } from "~/i18n";
const NEW_VERSION_OPTION = "__new__";
export default function OpenHoursForm() {
const [refreshKey, setRefreshKey] = createSignal(0);
const openHours = createAsync(() => queryLokOpenHours(refreshKey()).catch(() => []));
const createSubmission = useSubmission(createLokOpenHours);
const deleteSubmission = useSubmission(deleteLokOpenHours);
const [selectedVersion, setSelectedVersion] = createSignal("");
const [name, setName] = createSignal("");
const [paragraph1, setParagraph1] = createSignal("");
const [paragraph2, setParagraph2] = createSignal("");
const [paragraph3, setParagraph3] = createSignal("");
const [paragraph4, setParagraph4] = createSignal("");
const [kitchenNotice, setKitchenNotice] = createSignal("");
const latestFive = createMemo(() => openHours() ?? []);
const selectedOpenHours = createMemo(() =>
latestFive().find(version => String(version.id) === selectedVersion())
);
createEffect(() => {
if (!createSubmission.result) return;
setRefreshKey(previous => previous + 1);
setSelectedVersion("");
});
createEffect(() => {
if (!deleteSubmission.result) return;
setRefreshKey(previous => previous + 1);
setSelectedVersion("");
});
createEffect(() => {
const versions = latestFive();
const current = selectedVersion();
if (versions.length === 0) {
if (current !== NEW_VERSION_OPTION) {
setSelectedVersion(NEW_VERSION_OPTION);
}
return;
}
if (current === NEW_VERSION_OPTION) {
return;
}
const hasCurrent = versions.some(version => String(version.id) === current);
if (!current || !hasCurrent) {
setSelectedVersion(String(versions[0].id));
}
});
const reuseSelected = () => {
const selected = selectedOpenHours();
if (!selected) {
setName("");
setParagraph1("");
setParagraph2("");
setParagraph3("");
setParagraph4("");
setKitchenNotice("");
return;
}
setName(selected.name);
setParagraph1(selected.paragraph1);
setParagraph2(selected.paragraph2);
setParagraph3(selected.paragraph3);
setParagraph4(selected.paragraph4);
setKitchenNotice(selected.kitchenNotice);
};
createEffect(() => {
reuseSelected();
});
return (
<section class="w-full max-w-3xl rounded-2xl border border-[#C99763] bg-[#F5D1A9] p-6 shadow-md">
<h2 class="text-2xl font-semibold text-[#4C250E]">{t("home.openHours.heading")}</h2>
<Show when={latestFive().length > 0} fallback={<p class="mt-3 text-[#70421E]">{t("home.openHours.empty")}</p>}>
<div class="mt-2 flex gap-2">
<select
id="open-hours-version"
class="min-w-0 flex-1 rounded-md border border-[#C99763] bg-[#FFF7EE] px-3 py-2 text-[#4C250E] focus:outline-none focus:ring-2 focus:ring-[#A56C38]"
value={selectedVersion()}
onInput={event => setSelectedVersion(event.currentTarget.value)}
>
<For each={latestFive()}>
{version => (
<option value={String(version.id)}>
{version.name || t("home.openHours.latest")} · {new Date(version.version).toLocaleString()}
</option>
)}
</For>
<option value={NEW_VERSION_OPTION}>{t("home.openHours.new")}</option>
</select>
</div>
<form action={deleteLokOpenHours} method="post" class="mt-3">
<input type="hidden" name="id" value={selectedOpenHours()?.id ?? ""} />
<button
type="submit"
disabled={!selectedOpenHours() || deleteSubmission.pending}
class="rounded-md border border-[#8E4F24] bg-[#EED5B8] px-4 py-2 text-[#70421E] hover:bg-[#E3A977] disabled:opacity-50 disabled:cursor-not-allowed"
>
{t("home.openHours.delete")}
</button>
</form>
</Show>
<form action={createLokOpenHours} method="post" class="mt-5 space-y-3">
<div>
<label for="name" class="block text-sm font-medium text-[#4C250E]">{t("home.openHours.name")}</label>
<input
id="name"
name="name"
required
value={name()}
onInput={event => setName(event.currentTarget.value)}
class="mt-1 w-full rounded-md border border-[#C99763] bg-[#FFF7EE] px-3 py-2 text-[#4C250E] focus:outline-none focus:ring-2 focus:ring-[#A56C38]"
/>
<Show when={!name().trim()}>
<p class="mt-1 text-xs text-[#8E4F24]">{t("home.openHours.nameRequired")}</p>
</Show>
</div>
<div>
<label for="paragraph1" class="block text-sm font-medium text-[#4C250E]">{t("home.openHours.paragraph1")}</label>
<textarea
id="paragraph1"
name="paragraph1"
rows={2}
value={paragraph1()}
onInput={event => setParagraph1(event.currentTarget.value)}
class="mt-1 w-full rounded-md border border-[#C99763] bg-[#FFF7EE] px-3 py-2 text-[#4C250E] focus:outline-none focus:ring-2 focus:ring-[#A56C38]"
/>
</div>
<div>
<label for="paragraph2" class="block text-sm font-medium text-[#4C250E]">{t("home.openHours.paragraph2")}</label>
<textarea
id="paragraph2"
name="paragraph2"
rows={2}
value={paragraph2()}
onInput={event => setParagraph2(event.currentTarget.value)}
class="mt-1 w-full rounded-md border border-[#C99763] bg-[#FFF7EE] px-3 py-2 text-[#4C250E] focus:outline-none focus:ring-2 focus:ring-[#A56C38]"
/>
</div>
<div>
<label for="paragraph3" class="block text-sm font-medium text-[#4C250E]">{t("home.openHours.paragraph3")}</label>
<textarea
id="paragraph3"
name="paragraph3"
rows={2}
value={paragraph3()}
onInput={event => setParagraph3(event.currentTarget.value)}
class="mt-1 w-full rounded-md border border-[#C99763] bg-[#FFF7EE] px-3 py-2 text-[#4C250E] focus:outline-none focus:ring-2 focus:ring-[#A56C38]"
/>
</div>
<div>
<label for="paragraph4" class="block text-sm font-medium text-[#4C250E]">{t("home.openHours.paragraph4")}</label>
<textarea
id="paragraph4"
name="paragraph4"
rows={2}
value={paragraph4()}
onInput={event => setParagraph4(event.currentTarget.value)}
class="mt-1 w-full rounded-md border border-[#C99763] bg-[#FFF7EE] px-3 py-2 text-[#4C250E] focus:outline-none focus:ring-2 focus:ring-[#A56C38]"
/>
</div>
<div>
<label for="kitchenNotice" class="block text-sm font-medium text-[#4C250E]">{t("home.openHours.kitchenNotice")}</label>
<textarea
id="kitchenNotice"
name="kitchenNotice"
rows={2}
value={kitchenNotice()}
onInput={event => setKitchenNotice(event.currentTarget.value)}
class="mt-1 w-full rounded-md border border-[#C99763] bg-[#FFF7EE] px-3 py-2 text-[#4C250E] focus:outline-none focus:ring-2 focus:ring-[#A56C38]"
/>
</div>
<div class="flex flex-wrap gap-3 pt-1">
<button
type="button"
disabled={!selectedOpenHours()}
onClick={reuseSelected}
class="rounded-md border border-[#A56C38] bg-[#EED5B8] px-4 py-2 text-[#70421E] hover:bg-[#E3A977] disabled:opacity-50 disabled:cursor-not-allowed"
>
{t("home.openHours.reuse")}
</button>
<button
type="submit"
disabled={createSubmission.pending}
class="rounded-md border border-[#70421E] bg-[#8E4F24] px-4 py-2 text-[#FFF7EE] hover:bg-[#70421E] disabled:opacity-50 disabled:cursor-not-allowed"
>
{createSubmission.pending ? t("home.openHours.saving") : t("home.openHours.submit")}
</button>
</div>
<Show when={createSubmission.result}>
<p class="text-sm text-[#4C250E]">{t("home.openHours.saved")}</p>
</Show>
<Show when={deleteSubmission.result}>
<p class="text-sm text-[#4C250E]">{t("home.openHours.deleted")}</p>
</Show>
<Show when={createSubmission.error} keyed>
{error => <p class="text-sm text-[#8E4F24]">{error.message}</p>}
</Show>
<Show when={deleteSubmission.error} keyed>
{error => <p class="text-sm text-[#8E4F24]">{error.message}</p>}
</Show>
</form>
</section>
);
}

View File

@@ -15,9 +15,26 @@ const translations = {
"nav.language.en": "EN", "nav.language.en": "EN",
"meta.description": "SolidStart with-auth example", "meta.description": "SolidStart with-auth example",
"home.title": "Home", "home.title": "Home",
"home.heading": "Hello World", "home.heading": "KlAPI",
"home.signedInAs": "You are signed in as", "home.signedInAs": "You are signed in as",
"home.logoAlt": "logo", "home.logoAlt": "logo",
"home.openHours.heading": "Open hours versions",
"home.openHours.latest": "Latest",
"home.openHours.new": "New version",
"home.openHours.reuse": "Reuse selected",
"home.openHours.delete": "Delete selected",
"home.openHours.empty": "No open-hours versions found yet",
"home.openHours.name": "Version name",
"home.openHours.nameRequired": "Version name is required",
"home.openHours.paragraph1": "Paragraph 1",
"home.openHours.paragraph2": "Paragraph 2",
"home.openHours.paragraph3": "Paragraph 3",
"home.openHours.paragraph4": "Paragraph 4",
"home.openHours.kitchenNotice": "Kitchen notice",
"home.openHours.submit": "Add new version",
"home.openHours.saving": "Saving...",
"home.openHours.saved": "New version saved",
"home.openHours.deleted": "Version deleted",
"about.title": "About", "about.title": "About",
"about.apiVersion": "API version", "about.apiVersion": "API version",
"about.loading": "Loading...", "about.loading": "Loading...",
@@ -47,9 +64,26 @@ const translations = {
"nav.language.en": "EN", "nav.language.en": "EN",
"meta.description": "SolidStart with-auth -esimerkki", "meta.description": "SolidStart with-auth -esimerkki",
"home.title": "Etusivu", "home.title": "Etusivu",
"home.heading": "Hei maailma", "home.heading": "KlAPI",
"home.signedInAs": "Olet kirjautunut käyttäjänä", "home.signedInAs": "Olet kirjautunut käyttäjänä",
"home.logoAlt": "logo", "home.logoAlt": "logo",
"home.openHours.heading": "Aukioloaikaversiot",
"home.openHours.latest": "Viimeisin",
"home.openHours.new": "Uusi versio",
"home.openHours.reuse": "Käytä valittua uudelleen",
"home.openHours.delete": "Poista valittu",
"home.openHours.empty": "Aukioloaikaversioita ei vielä löytynyt",
"home.openHours.name": "Version nimi",
"home.openHours.nameRequired": "Version nimi on pakollinen",
"home.openHours.paragraph1": "Kappale 1",
"home.openHours.paragraph2": "Kappale 2",
"home.openHours.paragraph3": "Kappale 3",
"home.openHours.paragraph4": "Kappale 4",
"home.openHours.kitchenNotice": "Keittiöhuomio",
"home.openHours.submit": "Lisää uusi versio",
"home.openHours.saving": "Tallennetaan...",
"home.openHours.saved": "Uusi versio tallennettu",
"home.openHours.deleted": "Versio poistettu",
"about.title": "Tietoja", "about.title": "Tietoja",
"about.apiVersion": "API-versio", "about.apiVersion": "API-versio",
"about.loading": "Ladataan...", "about.loading": "Ladataan...",

View File

@@ -1,15 +1,14 @@
import { Title } from "@solidjs/meta"; import { Title } from "@solidjs/meta";
import { useAuth } from "~/components/Context"; import OpenHoursForm from "~/components/OpenHoursForm";
import { t } from "~/i18n"; import { t } from "~/i18n";
export default function Home() { export default function Home() {
const { session } = useAuth();
return ( return (
<main> <main>
<Title>{t("home.title")}</Title> <Title>{t("home.title")}</Title>
<h1 class="text-center">{t("home.heading")}</h1> <h1 class="text-center">{t("home.heading")}</h1>
<img src="/favicon.svg" alt={t("home.logoAlt")} class="w-28" /> <img src="/favicon.svg" alt={t("home.logoAlt")} class="w-28" />
<OpenHoursForm />
</main> </main>
); );
} }