Compare commits
8 commits
4ed65a8034
...
07f0999a3d
Author | SHA1 | Date | |
---|---|---|---|
07f0999a3d | |||
b1b91afab5 | |||
0712c789d4 | |||
1f4ca9a3b7 | |||
5ab3114c40 | |||
bfd0647493 | |||
3f31d24824 | |||
d403da0c9c |
75 changed files with 512 additions and 309 deletions
|
@ -10,7 +10,6 @@ RUN npm install
|
|||
|
||||
COPY . .
|
||||
|
||||
RUN npm run migrate
|
||||
RUN npm run build
|
||||
|
||||
ENV HOST=0.0.0.0
|
||||
|
|
16
compose.yaml
16
compose.yaml
|
@ -4,8 +4,18 @@ services:
|
|||
ports:
|
||||
- "8000:8000"
|
||||
environment:
|
||||
DATABASE_PATH: /var/sqlite/reset-sender-v2.sqlite
|
||||
DATABASE_URL: postgres://reset_sender_v2:reset_sender_v2@postgres:5432/reset_sender_v2
|
||||
postgres:
|
||||
image: postgres:14-alpine
|
||||
volumes:
|
||||
- sqlite_data:/var/sqlite
|
||||
- postgres_data:/var/lib/postgresql/data/
|
||||
environment:
|
||||
POSTGRES_DB: reset_sender_v2
|
||||
POSTGRES_USER: reset_sender_v2
|
||||
POSTGRES_PASSWORD: reset_sender_v2
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- '5432:5432'
|
||||
|
||||
volumes:
|
||||
sqlite_data:
|
||||
postgres_data:
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import "dotenv/config";
|
||||
import { drizzle } from "drizzle-orm/postgres-js";
|
||||
import { migrate } from "drizzle-orm/postgres-js/migrator";
|
||||
import postgres from "postgres";
|
||||
import { db, client } from "../src/db";
|
||||
|
||||
const migrationClient = postgres(process.env.DATABASE_URL!, { max: 1 });
|
||||
migrate(drizzle(migrationClient), { migrationsFolder: "drizzle" });
|
||||
await migrate(db, { migrationsFolder: "drizzle" });
|
||||
|
||||
await client.end();
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
name: DONAU Versicherung
|
||||
lang: de
|
||||
regions: [au]
|
||||
template: de
|
||||
emails:
|
||||
- donau@donauversicherung.at
|
||||
- j.havasi@donauversicherung.at
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
name: WIENER STÄDTISCHE
|
||||
lang: de
|
||||
regions: [au]
|
||||
template: de
|
||||
emails:
|
||||
- kundenservice@wienerstaedtische.at
|
||||
- r.mueller@wienerstaedtische.at
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
name: CPP
|
||||
lang: cs
|
||||
regions: [cz]
|
||||
template: cs
|
||||
emails:
|
||||
- info@cpp.cz
|
||||
- pavel.wiesner@cpp.cz
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
name: Kooperativa
|
||||
lang: cs
|
||||
regions: [cz]
|
||||
template: cs
|
||||
emails:
|
||||
- info@koop.cz
|
||||
- mdivis@koop.cz
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
name: Alfa
|
||||
lang: hu
|
||||
regions: [hu]
|
||||
template: hu
|
||||
emails:
|
||||
- peter.zatyko@alfa.hu
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
name: Union Biztosito
|
||||
lang: hu
|
||||
regions: [hu]
|
||||
template: hu
|
||||
emails:
|
||||
- info@union.hu
|
||||
- gabor.havas@union.hu
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
name: Compensa
|
||||
lang: lt
|
||||
regions: [lt]
|
||||
template: lt
|
||||
emails:
|
||||
- info@compensa.lt
|
||||
- deividas.raipa@compensa.lt
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
name: Donaris
|
||||
lang: ro
|
||||
regions: [ro]
|
||||
template: ro
|
||||
emails:
|
||||
- office@donaris.md
|
||||
- d.gherasim@donaris.md
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
name: Compensa
|
||||
lang: pl
|
||||
regions: [pl]
|
||||
template: pl
|
||||
emails:
|
||||
- prasa@compensa.pl
|
||||
- anna.wlodarczyk-moczkowska@compensa.pl
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
name: Interrisk
|
||||
lang: pl
|
||||
regions: [pl]
|
||||
template: pl
|
||||
emails:
|
||||
- sekretariat@interrisk.pl
|
||||
- piotr.narloch@interrisk.pl
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
name: Wiener
|
||||
lang: pl
|
||||
regions: [pl]
|
||||
template: pl
|
||||
emails:
|
||||
- kontakt@wiener.pl
|
||||
- adwalcki@wiener.pl
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
name: Asirom
|
||||
lang: ro
|
||||
regions: [ro]
|
||||
template: ro
|
||||
emails:
|
||||
- comunicare@asirom.ro
|
||||
- madalin.rosu@asirom.ro
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
name: Omniasig
|
||||
lang: ro
|
||||
regions: [ro]
|
||||
template: ro
|
||||
emails:
|
||||
- office@omniasig.ro
|
||||
- mihai.tecau@omniasig.ro
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
name: Komunálna poisťovňa
|
||||
lang: sk
|
||||
regions: [sk]
|
||||
template: sk
|
||||
emails:
|
||||
- sekretariatgr@kpas.sk
|
||||
- slavka.miklosova@kpas.sk
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
name: Kooperativa
|
||||
lang: sk
|
||||
regions: [sk]
|
||||
template: sk
|
||||
emails:
|
||||
- sekretariatgr.koop@koop.sk
|
||||
- vladimirbakes@koop.sk
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
name: VIG (Holding)
|
||||
lang: en
|
||||
messageTemplate: vigHolding
|
||||
regions: [intl, au]
|
||||
template: vigHolding
|
||||
ceo: Hartwig Löger
|
||||
emails:
|
||||
- hartwig.loeger@vig.com
|
||||
|
|
|
@ -1,5 +1,19 @@
|
|||
import { z, defineCollection } from "astro:content";
|
||||
import { LANGUAGES } from "../config";
|
||||
|
||||
const regions = defineCollection({
|
||||
type: "data",
|
||||
schema: z.object({
|
||||
title: z.string(),
|
||||
languages: z.array(z.string()),
|
||||
}),
|
||||
});
|
||||
|
||||
const languages = defineCollection({
|
||||
type: "data",
|
||||
schema: z.object({
|
||||
title: z.string(),
|
||||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
* Branches are classified in subdirectories according to their country.
|
||||
|
@ -10,16 +24,8 @@ const branches = defineCollection({
|
|||
type: "data",
|
||||
schema: z.object({
|
||||
name: z.string(),
|
||||
/**
|
||||
* Determines the language of the letter. This can be different from the chosen page language.
|
||||
*/
|
||||
lang: z.string(),
|
||||
/**
|
||||
* Usually template is determined by language. In some special cases, however, it is useful
|
||||
* to use some specific template per branch. In such case `messageTemplate` overrides `lang`.
|
||||
*/
|
||||
messageTemplate: z.string().optional(),
|
||||
|
||||
template: z.string(),
|
||||
regions: z.array(z.string()),
|
||||
emails: z.array(z.string()).nonempty(),
|
||||
}),
|
||||
});
|
||||
|
@ -55,6 +61,8 @@ const emailConfirmed = defineCollection({
|
|||
});
|
||||
|
||||
export const collections = {
|
||||
regions,
|
||||
languages,
|
||||
branches,
|
||||
variants,
|
||||
intro,
|
||||
|
|
1
src/content/languages/cs.yaml
Normal file
1
src/content/languages/cs.yaml
Normal file
|
@ -0,0 +1 @@
|
|||
title: Česky
|
1
src/content/languages/de.yaml
Normal file
1
src/content/languages/de.yaml
Normal file
|
@ -0,0 +1 @@
|
|||
title: Deutsch
|
1
src/content/languages/en.yaml
Normal file
1
src/content/languages/en.yaml
Normal file
|
@ -0,0 +1 @@
|
|||
title: English
|
1
src/content/languages/hu.yaml
Normal file
1
src/content/languages/hu.yaml
Normal file
|
@ -0,0 +1 @@
|
|||
title: Magyar
|
1
src/content/languages/lt.yaml
Normal file
1
src/content/languages/lt.yaml
Normal file
|
@ -0,0 +1 @@
|
|||
title: Lietuviškas
|
1
src/content/languages/pl.yaml
Normal file
1
src/content/languages/pl.yaml
Normal file
|
@ -0,0 +1 @@
|
|||
title: Polski
|
1
src/content/languages/ro.yaml
Normal file
1
src/content/languages/ro.yaml
Normal file
|
@ -0,0 +1 @@
|
|||
title: Românesc
|
1
src/content/languages/sk.yaml
Normal file
1
src/content/languages/sk.yaml
Normal file
|
@ -0,0 +1 @@
|
|||
title: Slovensky
|
2
src/content/regions/au.yaml
Normal file
2
src/content/regions/au.yaml
Normal file
|
@ -0,0 +1,2 @@
|
|||
title: Austria
|
||||
languages: [de]
|
2
src/content/regions/cz.yaml
Normal file
2
src/content/regions/cz.yaml
Normal file
|
@ -0,0 +1,2 @@
|
|||
title: Czechia
|
||||
languages: [cs]
|
2
src/content/regions/hu.yaml
Normal file
2
src/content/regions/hu.yaml
Normal file
|
@ -0,0 +1,2 @@
|
|||
title: Hungary
|
||||
languages: [hu]
|
2
src/content/regions/intl.yaml
Normal file
2
src/content/regions/intl.yaml
Normal file
|
@ -0,0 +1,2 @@
|
|||
title: International
|
||||
languages: [en]
|
2
src/content/regions/lt.yaml
Normal file
2
src/content/regions/lt.yaml
Normal file
|
@ -0,0 +1,2 @@
|
|||
title: Lithuania
|
||||
languages: [lt]
|
2
src/content/regions/md.yaml
Normal file
2
src/content/regions/md.yaml
Normal file
|
@ -0,0 +1,2 @@
|
|||
title: Moldova
|
||||
languages: [ro]
|
2
src/content/regions/pl.yaml
Normal file
2
src/content/regions/pl.yaml
Normal file
|
@ -0,0 +1,2 @@
|
|||
title: Poland
|
||||
languages: [pl]
|
2
src/content/regions/ro.yaml
Normal file
2
src/content/regions/ro.yaml
Normal file
|
@ -0,0 +1,2 @@
|
|||
title: Romania
|
||||
languages: [ro]
|
2
src/content/regions/sk.yaml
Normal file
2
src/content/regions/sk.yaml
Normal file
|
@ -0,0 +1,2 @@
|
|||
title: Slovakia
|
||||
languages: [sk]
|
|
@ -1,12 +0,0 @@
|
|||
import type { Lang } from "./lang";
|
||||
|
||||
export const countries: Record<Lang, string> = {
|
||||
en: "International (english)",
|
||||
cs: "Czechia",
|
||||
sk: "Slovakia",
|
||||
pl: "Poland",
|
||||
hu: "Hungary",
|
||||
ro: "Romania/Moldova",
|
||||
lt: "Lithuania",
|
||||
de: "Austria",
|
||||
};
|
|
@ -2,5 +2,11 @@ import { drizzle } from "drizzle-orm/postgres-js";
|
|||
import postgres from "postgres";
|
||||
import * as schema from "./schema";
|
||||
|
||||
const client = postgres(import.meta.env.DATABASE_URL);
|
||||
const DATABASE_URL = process.env.DATABASE_URL;
|
||||
|
||||
if (!DATABASE_URL) {
|
||||
throw new Error("'DATABASE_URL' environment variable not set");
|
||||
}
|
||||
|
||||
export const client = postgres(DATABASE_URL);
|
||||
export const db = drizzle(client, { schema });
|
||||
|
|
29
src/lang.ts
29
src/lang.ts
|
@ -1,7 +1,32 @@
|
|||
import { z } from "zod";
|
||||
import { LANGUAGES } from "./config.ts";
|
||||
|
||||
export type Lang = (typeof LANGUAGES)[number];
|
||||
|
||||
export function isKnownLang(str: string): str is Lang {
|
||||
return (LANGUAGES as ReadonlyArray<string>).includes(str);
|
||||
export const langSchema = z.union(LANGUAGES.map((lang) => z.literal(lang)));
|
||||
|
||||
export class UnknownLanguageError extends Error {
|
||||
constructor(lang: string | undefined) {
|
||||
super(`Unknown language '${lang}'`);
|
||||
}
|
||||
}
|
||||
|
||||
export function tryGetWebsiteLang(params: Record<string, string | undefined>): Lang | undefined {
|
||||
const { lang } = params;
|
||||
if (lang && !isKnownLang(lang)) {
|
||||
throw new UnknownLanguageError(lang);
|
||||
}
|
||||
return lang as Lang;
|
||||
}
|
||||
|
||||
export function getWebsiteLang(params: Record<string, string | undefined>): Lang {
|
||||
const { lang } = params;
|
||||
if (!isKnownLang(lang)) {
|
||||
throw new UnknownLanguageError(lang);
|
||||
}
|
||||
return lang;
|
||||
}
|
||||
|
||||
export function isKnownLang(str: unknown): str is Lang {
|
||||
return typeof str === "string" && (LANGUAGES as ReadonlyArray<string>).includes(str);
|
||||
}
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
---
|
||||
import { DEFAULT_LANG } from "../config";
|
||||
import { makeT } from "../i18n";
|
||||
import type { Lang } from "../lang";
|
||||
import { tryGetWebsiteLang, type Lang } from "../lang";
|
||||
import Footer from "../views/layout/Footer.astro";
|
||||
|
||||
interface Props {
|
||||
lang?: Lang;
|
||||
}
|
||||
|
||||
const { lang = DEFAULT_LANG } = Astro.props;
|
||||
const lang = tryGetWebsiteLang(Astro.params) ?? DEFAULT_LANG;
|
||||
const { t } = makeT(lang);
|
||||
|
||||
const title = t("website_name");
|
||||
|
|
48
src/letter/GenderInput.astro
Normal file
48
src/letter/GenderInput.astro
Normal file
|
@ -0,0 +1,48 @@
|
|||
---
|
||||
import { makeT } from "../i18n";
|
||||
import type { Lang } from "../lang";
|
||||
import Radio from "../ui/Radio.astro";
|
||||
import RadioGroup from "../ui/RadioGroup.astro";
|
||||
import type { Options } from "./request";
|
||||
|
||||
interface Props {
|
||||
websiteLang: Lang;
|
||||
letterLang: Lang;
|
||||
options: Options;
|
||||
}
|
||||
|
||||
const { websiteLang, options, ...props } = Astro.props;
|
||||
const { t } = makeT(websiteLang);
|
||||
|
||||
const genderOptions: Record<Lang, Array<"f" | "n" | "m">> = {
|
||||
en: [],
|
||||
cs: ["f", "m", "n"],
|
||||
sk: ["m", "f", "n"],
|
||||
pl: ["m", "f", "n"],
|
||||
hu: [],
|
||||
ro: ["m", "f", "n"],
|
||||
lt: ["m", "f", "n"],
|
||||
de: ["m", "f", "n"],
|
||||
};
|
||||
|
||||
const availableGenders = genderOptions[props.letterLang];
|
||||
---
|
||||
|
||||
<RadioGroup
|
||||
required
|
||||
id="gender-input"
|
||||
label={t("form.gender")}
|
||||
class:list={{ hidden: !availableGenders || availableGenders.length === 0 }}
|
||||
{...props}
|
||||
>
|
||||
{
|
||||
availableGenders.map((gender) => (
|
||||
<Radio
|
||||
name="gender"
|
||||
value={gender}
|
||||
label={t(`form.gender.${gender}`)}
|
||||
checked={options.gender === gender}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</RadioGroup>
|
|
@ -1,40 +1,37 @@
|
|||
---
|
||||
import { makeT } from "../i18n";
|
||||
import { makeT, type TranslationKey } from "../i18n";
|
||||
import Button from "../ui/Button.astro";
|
||||
import { letterOptionsSchema, type LetterOptions } from "./schema";
|
||||
import LoadingIndicator from "./LoadingIndicator.astro";
|
||||
import { renderText } from "./templates";
|
||||
import type { Lang } from "../lang";
|
||||
import type { Options } from "./request";
|
||||
import { optionsValidationSchema } from "./schema";
|
||||
|
||||
interface Props {
|
||||
lang: Lang;
|
||||
options?: Partial<LetterOptions>;
|
||||
defaultBranchId: string;
|
||||
websiteLang: Lang;
|
||||
letterLang: Lang;
|
||||
options: Options;
|
||||
message: string;
|
||||
}
|
||||
|
||||
const { lang, options = {}, defaultBranchId } = Astro.props;
|
||||
const { t } = makeT(lang);
|
||||
const result = letterOptionsSchema.safeParse(options);
|
||||
|
||||
const letterText = renderText(lang, {
|
||||
...options,
|
||||
branch: options?.branch ?? defaultBranchId,
|
||||
});
|
||||
const { websiteLang, letterLang, options, message, ...props } = Astro.props;
|
||||
const { t } = makeT(websiteLang);
|
||||
const validationResult = await optionsValidationSchema.safeParseAsync(options);
|
||||
---
|
||||
|
||||
<form id="letter-body">
|
||||
<form id="letter-body" {...props}>
|
||||
<div class="mb-6">
|
||||
<label class="block text-base mb-2" for="letter-input">
|
||||
{t("form.message")}
|
||||
</label>
|
||||
<div class="relative overflow-hidden rounded-[3px]">
|
||||
<textarea
|
||||
lang={letterLang}
|
||||
name="message"
|
||||
id="letter-input"
|
||||
cols={60}
|
||||
rows={24}
|
||||
class="block w-full px-4 py-3 bg-white text-black text-lg"
|
||||
style="white-space: pre-wrap">{letterText}</textarea
|
||||
style="white-space: pre-wrap">{message}</textarea
|
||||
>
|
||||
<LoadingIndicator
|
||||
id="letter-progress-bar"
|
||||
|
@ -42,11 +39,11 @@ const letterText = renderText(lang, {
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex flex-wrap items-center gap-4">
|
||||
<Button
|
||||
class="flex w-full md:w-auto md:mx-auto lg:mx-0 md:min-w-[260px]"
|
||||
type="button"
|
||||
disabled={!result.success}
|
||||
disabled={!validationResult.success}
|
||||
onclick="document.querySelector('#contact-details-dialog')?.showModal()"
|
||||
>
|
||||
{t("form.send_letter")}
|
||||
|
@ -56,5 +53,13 @@ const letterText = renderText(lang, {
|
|||
></path>
|
||||
</svg>
|
||||
</Button>
|
||||
{
|
||||
!validationResult.success && (
|
||||
<p>
|
||||
{t(`form.${validationResult.error.issues[0].path}` as TranslationKey)}
|
||||
{validationResult.error.issues[0].message}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</form>
|
||||
|
|
25
src/letter/LetterBranchInput.astro
Normal file
25
src/letter/LetterBranchInput.astro
Normal file
|
@ -0,0 +1,25 @@
|
|||
---
|
||||
import Select from "../ui/Select.astro";
|
||||
import type { Lang } from "../lang";
|
||||
import { type BranchOption } from "./branches";
|
||||
|
||||
interface Props {
|
||||
branchOptions: Array<BranchOption>;
|
||||
branchId?: string;
|
||||
}
|
||||
|
||||
const { branchOptions, branchId } = Astro.props;
|
||||
---
|
||||
|
||||
<Select name="branch" id="branch-input" class="mb-6">
|
||||
{
|
||||
branchOptions.map((branch, index) => {
|
||||
const selected = branchId ? branch.id === branchId : index === 0;
|
||||
return (
|
||||
<option value={branch.id} selected={selected}>
|
||||
{branch.name} ({branch.region})
|
||||
</option>
|
||||
);
|
||||
})
|
||||
}
|
||||
</Select>
|
|
@ -4,35 +4,39 @@ import LetterOptionsForm from "./LetterOptionsForm.astro";
|
|||
import LetterVariantInput from "./LetterVariantInput.astro";
|
||||
import LetterContactDetails from "./LetterContactDetails.astro";
|
||||
import type { Lang } from "../lang";
|
||||
import { getBranches } from "./branches";
|
||||
import type { Options } from "./request";
|
||||
import { getMessage } from "./templates";
|
||||
import { getBranchOptions, type BranchOption } from "./branches";
|
||||
import { getEntry } from "astro:content";
|
||||
|
||||
interface Props {
|
||||
lang: Lang;
|
||||
websiteLang: Lang;
|
||||
options: Options;
|
||||
}
|
||||
|
||||
const { lang } = Astro.props;
|
||||
|
||||
const branches = await getBranches(lang);
|
||||
const defaultBranchId = branches[0].id;
|
||||
const { websiteLang, options } = Astro.props;
|
||||
const branchOptions = await getBranchOptions(websiteLang);
|
||||
const selectedBranch = (await getEntry("branches", options.branchId ?? branchOptions[0].id))!;
|
||||
const { message, letterLang } = getMessage({ websiteLang, options, selectedBranch });
|
||||
---
|
||||
|
||||
<div
|
||||
id="letter-builder"
|
||||
hx-get={`/${lang}/letter`}
|
||||
hx-get={`/${websiteLang}/letter`}
|
||||
hx-trigger="change"
|
||||
hx-target="#letter-body"
|
||||
hx-include="#letter-variant-input [name], #letter-options [name]"
|
||||
hx-indicator="#letter-progress-bar"
|
||||
>
|
||||
<div class="bg-black/50 px-6 py-6 -mx-6 rounded-t theme-inverted">
|
||||
<LetterVariantInput {lang} />
|
||||
<LetterVariantInput {websiteLang} />
|
||||
</div>
|
||||
<div class="grid lg:grid-cols-3 items-start gap-12 bg-black/10 px-6 py-6 -mx-6 rounded-b mb-12">
|
||||
<LetterOptionsForm {lang} {branches} {defaultBranchId} />
|
||||
<LetterOptionsForm {websiteLang} {letterLang} {branchOptions} {options} />
|
||||
<div class="lg:col-span-2">
|
||||
<LetterBodyForm {lang} {defaultBranchId} />
|
||||
<LetterBodyForm {message} {websiteLang} {letterLang} {options} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<LetterContactDetails {lang} />
|
||||
<LetterContactDetails {websiteLang} />
|
||||
|
|
|
@ -8,17 +8,17 @@ import Field from "../ui/Field.astro";
|
|||
import LoadingIndicator from "./LoadingIndicator.astro";
|
||||
|
||||
interface Props {
|
||||
lang: Lang;
|
||||
websiteLang: Lang;
|
||||
}
|
||||
|
||||
const { lang } = Astro.props;
|
||||
const { t, jt } = makeT(lang);
|
||||
const { websiteLang } = Astro.props;
|
||||
const { t, jt } = makeT(websiteLang);
|
||||
---
|
||||
|
||||
<Dialog id="contact-details-dialog" heading={t("form.few_more_details")}>
|
||||
<LoadingIndicator slot="top" id="save-progress-bar" class="sticky top-0 left-0 right-0" />
|
||||
<form
|
||||
hx-post={`/${lang}/save`}
|
||||
hx-post={`/${websiteLang}/save`}
|
||||
hx-include="#letter-variant-input [name], #letter-options [name], #letter-body [name]"
|
||||
hx-target="#contact-details-dialog [data-dialog-content]"
|
||||
hx-indicator="#save-progress-bar"
|
||||
|
@ -32,7 +32,7 @@ const { t, jt } = makeT(lang);
|
|||
jt(
|
||||
"form.agree_with_privacy_policy",
|
||||
// prettier-ignore
|
||||
<a href="/gdpr" target="_blank" class="link">{t("form.privacy_policy")}</a>,
|
||||
<a href="/gdpr" target="_blank" class="link">{t("form.privacy_policy")}</a>
|
||||
)
|
||||
}
|
||||
</Checkbox>
|
||||
|
|
|
@ -1,75 +1,37 @@
|
|||
---
|
||||
import type { CollectionEntry } from "astro:content";
|
||||
import Field from "../ui/Field.astro";
|
||||
import Radio from "../ui/Radio.astro";
|
||||
import RadioGroup from "../ui/RadioGroup.astro";
|
||||
import { makeT } from "../i18n";
|
||||
import Label from "../ui/Label.astro";
|
||||
import Select from "../ui/Select.astro";
|
||||
import type { Lang } from "../lang";
|
||||
import Checkbox from "../ui/Checkbox.astro";
|
||||
import { countries } from "../countries";
|
||||
import type { Options } from "./request";
|
||||
import GenderInput from "./GenderInput.astro";
|
||||
import LetterBranchInput from "./LetterBranchInput.astro";
|
||||
import type { BranchOption } from "./branches";
|
||||
|
||||
interface Props {
|
||||
lang: Lang;
|
||||
branches: Array<CollectionEntry<"branches">>;
|
||||
defaultBranchId: string;
|
||||
websiteLang: Lang;
|
||||
letterLang: Lang;
|
||||
branchOptions: Array<BranchOption>;
|
||||
options: Options;
|
||||
}
|
||||
|
||||
const { lang, branches, defaultBranchId } = Astro.props;
|
||||
const { t } = makeT(lang);
|
||||
|
||||
const genderOptions: Record<Lang, Array<"f" | "n" | "m">> = {
|
||||
en: [],
|
||||
cs: ["f", "m", "n"],
|
||||
sk: ["m", "f", "n"],
|
||||
pl: ["m", "f", "n"],
|
||||
hu: [],
|
||||
ro: ["m", "f", "n"],
|
||||
lt: ["m", "f", "n"],
|
||||
de: ["m", "f", "n"],
|
||||
};
|
||||
|
||||
const genders = genderOptions[lang];
|
||||
const { websiteLang, letterLang, branchOptions, options } = Astro.props;
|
||||
const { t } = makeT(websiteLang);
|
||||
---
|
||||
|
||||
<form id="letter-options" class="grid md:grid-cols-2 lg:grid-cols-1 gap-8" novalidate>
|
||||
<Field required label={t("form.first_name")} name="firstName" />
|
||||
<Field required label={t("form.last_name")} name="lastName" />
|
||||
|
||||
<Field required label={t("form.first_name")} name="firstName" value={options.firstName} />
|
||||
<Field required label={t("form.last_name")} name="lastName" value={options.lastName} />
|
||||
<div>
|
||||
<Label required class="mb-2" for="branch-input">
|
||||
{t("form.branch")}
|
||||
</Label>
|
||||
<Select name="branch" id="branch-input" class="mb-6">
|
||||
{
|
||||
branches.map((branch) => {
|
||||
return (
|
||||
<option value={branch.id} selected={branch.id === defaultBranchId}>
|
||||
{branch.data.name}
|
||||
{lang === "en" ? <span>– {countries[branch.data.lang as Lang]}</span> : null}
|
||||
</option>
|
||||
);
|
||||
})
|
||||
}
|
||||
</Select>
|
||||
<LetterBranchInput {branchOptions} branchId={options.branchId} />
|
||||
<Checkbox name="isClient">
|
||||
{t("form.is_client")}
|
||||
</Checkbox>
|
||||
</div>
|
||||
|
||||
{
|
||||
genders && genders.length > 0 && (
|
||||
<RadioGroup required label={t("form.gender")}>
|
||||
{genders.map((gender, index) => (
|
||||
<Radio
|
||||
name="gender"
|
||||
value={gender}
|
||||
label={t(`form.gender.${gender}`)}
|
||||
checked={index === 0}
|
||||
/>
|
||||
))}
|
||||
</RadioGroup>
|
||||
)
|
||||
}
|
||||
<GenderInput {websiteLang} {letterLang} {options} />
|
||||
</form>
|
||||
|
|
|
@ -5,12 +5,12 @@ import type { Lang } from "../lang";
|
|||
import Dialog from "../ui/Dialog.astro";
|
||||
|
||||
interface Props {
|
||||
lang: Lang;
|
||||
websiteLang: Lang;
|
||||
variant: CollectionEntry<"variants">;
|
||||
}
|
||||
|
||||
const { lang, variant } = Astro.props;
|
||||
const { t } = makeT(lang);
|
||||
const { websiteLang, variant } = Astro.props;
|
||||
const { t } = makeT(websiteLang);
|
||||
const { id } = variant.data;
|
||||
const { Content } = await variant.render();
|
||||
const slug = variant.slug.split("/")[1];
|
||||
|
|
|
@ -5,14 +5,14 @@ import type { Lang } from "../lang";
|
|||
import LetterVariantButton from "./LetterVariantButton.astro";
|
||||
|
||||
interface Props {
|
||||
lang: Lang;
|
||||
websiteLang: Lang;
|
||||
}
|
||||
|
||||
const { lang } = Astro.props;
|
||||
const { t } = makeT(lang);
|
||||
const { websiteLang } = Astro.props;
|
||||
const { t } = makeT(websiteLang);
|
||||
|
||||
const variants = (
|
||||
await getCollection("variants", (variant) => variant.id.startsWith(`${lang}/`))
|
||||
await getCollection("variants", (variant) => variant.id.startsWith(`${websiteLang}/`))
|
||||
).toSorted((a, b) => a.data.id - b.data.id);
|
||||
---
|
||||
|
||||
|
@ -22,7 +22,11 @@ const variants = (
|
|||
{t("form.variant")}
|
||||
</legend>
|
||||
<div class="grid md:grid-cols-3 gap-2">
|
||||
{variants.map((variant) => <LetterVariantButton lang={lang} variant={variant} />)}
|
||||
{
|
||||
variants.map((variant) => (
|
||||
<LetterVariantButton websiteLang={websiteLang} variant={variant} />
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
|
|
|
@ -1,17 +1,29 @@
|
|||
import { getCollection } from "astro:content";
|
||||
import { getCollection, getEntry } from "astro:content";
|
||||
import type { Lang } from "../lang";
|
||||
|
||||
export async function getBranches(lang: Lang) {
|
||||
// For international version, we list all branches, but display letter in english
|
||||
if (lang === "en") {
|
||||
const branches = await getCollection("branches");
|
||||
return branches.sort((a, b) => a.data.name.localeCompare(b.data.name));
|
||||
}
|
||||
|
||||
const branches = await getCollection(
|
||||
"branches",
|
||||
(branch) => branch.data.lang === "en" || branch.data.lang === lang
|
||||
);
|
||||
|
||||
return branches.sort((a, b) => a.data.name.localeCompare(b.data.name));
|
||||
export interface BranchOption {
|
||||
id: string;
|
||||
name: string;
|
||||
region: string;
|
||||
}
|
||||
|
||||
export async function getBranchOptions(websiteLang: Lang): Promise<Array<BranchOption>> {
|
||||
const allRegions = await getCollection("regions");
|
||||
const allBranches = await getCollection("branches");
|
||||
|
||||
return allRegions
|
||||
.filter(
|
||||
(region) =>
|
||||
websiteLang === "en" || region.id === "intl" || region.data.languages.includes(websiteLang)
|
||||
)
|
||||
.flatMap((region) => {
|
||||
const regionalBranches = allBranches.filter((b) => b.data.regions.includes(region.id));
|
||||
return regionalBranches.map((branch) => {
|
||||
return {
|
||||
id: branch.id,
|
||||
name: branch.data.name,
|
||||
region: region.data.title,
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
29
src/letter/request.ts
Normal file
29
src/letter/request.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
import { z } from "zod";
|
||||
import type { Lang } from "../lang";
|
||||
import { optionsSchema } from "./schema";
|
||||
|
||||
export type Options = z.TypeOf<typeof optionsSchema>;
|
||||
|
||||
export async function parseOptionsFromRequest(
|
||||
params: URLSearchParams
|
||||
): Promise<{ status: "notFound" } | { status: "ok"; options: Options }> {
|
||||
const options = {
|
||||
firstName: params.get("firstName") || undefined,
|
||||
lastName: params.get("lastName") || undefined,
|
||||
variant: params.get("variant") || undefined,
|
||||
gender: params.get("gender") || undefined,
|
||||
branchId: params.get("branch") || undefined,
|
||||
isClient: params.has("isClient"),
|
||||
};
|
||||
|
||||
const result = await optionsSchema.safeParseAsync(options);
|
||||
if (!result.success) {
|
||||
return { status: "notFound" };
|
||||
}
|
||||
const parsed = result.data;
|
||||
|
||||
return {
|
||||
status: "ok",
|
||||
options: parsed,
|
||||
};
|
||||
}
|
|
@ -1,22 +1,22 @@
|
|||
import { z } from "zod";
|
||||
import { checkbox } from "../lib/types";
|
||||
|
||||
export const letterOptionsSchema = z.object({
|
||||
firstName: z.string().min(1),
|
||||
lastName: z.string().min(1),
|
||||
variant: z.string().min(1),
|
||||
gender: z.string().min(1).optional(),
|
||||
branch: z.string().min(1),
|
||||
isClient: checkbox(),
|
||||
export const variantSchema = z.union([z.literal("1"), z.literal("2"), z.literal("3")]);
|
||||
export const genderSchema = z.union([z.literal("m"), z.literal("f"), z.literal("n")]);
|
||||
|
||||
export const optionsSchema = z.object({
|
||||
firstName: z.string().min(1).optional(),
|
||||
lastName: z.string().min(1).optional(),
|
||||
variant: variantSchema.default("1"),
|
||||
gender: genderSchema.default("m"),
|
||||
branchId: z.string().min(1).optional(),
|
||||
isClient: z.boolean(),
|
||||
});
|
||||
|
||||
export type LetterOptions = z.TypeOf<typeof letterOptionsSchema>;
|
||||
export const optionsValidationSchema = optionsSchema.required();
|
||||
|
||||
export const letterFormSchema = letterOptionsSchema.extend({
|
||||
export const finalValidationSchema = optionsValidationSchema.extend({
|
||||
email: z.string().min(1),
|
||||
phone: z.string().optional(),
|
||||
message: z.string().min(1),
|
||||
subscribed: checkbox(),
|
||||
message: z.string().min(10),
|
||||
subscribed: z.boolean(),
|
||||
});
|
||||
|
||||
export type LetterForm = z.TypeOf<typeof letterFormSchema>;
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import type { TemplateFn } from "./types";
|
||||
|
||||
/**
|
||||
* @status done
|
||||
*/
|
||||
export const cs: TemplateFn = (c) => {
|
||||
export const lang = "cs";
|
||||
|
||||
export const render: TemplateFn = (c) => {
|
||||
// prettier-ignore
|
||||
return (
|
||||
`Dobrý den,
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import type { TemplateFn } from "./types";
|
||||
|
||||
/**
|
||||
* @status done
|
||||
*/
|
||||
export const de: TemplateFn = (c) => {
|
||||
export const lang = "de";
|
||||
|
||||
export const render: TemplateFn = (c) => {
|
||||
// prettier-ignore
|
||||
return (
|
||||
`Guten Tag,
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import type { TemplateFn } from "./types";
|
||||
|
||||
/**
|
||||
* @status done
|
||||
*/
|
||||
export const en: TemplateFn = (c) => {
|
||||
export const lang = "en";
|
||||
|
||||
export const render: TemplateFn = (c) => {
|
||||
// prettier-ignore
|
||||
return (
|
||||
`Hello,
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
import type { TemplateFn } from "./types";
|
||||
|
||||
/**
|
||||
* TODO: fix gender in opening
|
||||
* @status done
|
||||
*/
|
||||
export const hu: TemplateFn = (c) => {
|
||||
export const lang = "hu";
|
||||
|
||||
export const render: TemplateFn = (c) => {
|
||||
// prettier-ignore
|
||||
return (
|
||||
`Tisztelt Hölgyem,
|
||||
|
|
|
@ -1,56 +1,66 @@
|
|||
import type { LetterOptions } from "../schema";
|
||||
import type { TemplateFn } from "./types";
|
||||
import { getEntry, type CollectionEntry } from "astro:content";
|
||||
import type { Template, TemplateFn } from "./types";
|
||||
import { type CollectionEntry } from "astro:content";
|
||||
|
||||
import { vigHolding } from "./vig-holding";
|
||||
import { cs } from "./cs";
|
||||
import { de } from "./de";
|
||||
import { en } from "./en";
|
||||
import { hu } from "./hu";
|
||||
import { lt } from "./lt";
|
||||
import { pl } from "./pl";
|
||||
import { ro } from "./ro";
|
||||
import { sk } from "./sk";
|
||||
import * as vigHolding from "./vig-holding";
|
||||
import * as cs from "./cs";
|
||||
import * as de from "./de";
|
||||
import * as en from "./en";
|
||||
import * as hu from "./hu";
|
||||
import * as lt from "./lt";
|
||||
import * as pl from "./pl";
|
||||
import * as ro from "./ro";
|
||||
import * as sk from "./sk";
|
||||
import type { Lang } from "../../lang";
|
||||
import type { Options } from "../request";
|
||||
|
||||
const templates: Record<string, TemplateFn> = { vigHolding, cs, de, en, hu, lt, pl, ro, sk };
|
||||
const templates: Record<string, Template> = {
|
||||
vigHolding,
|
||||
cs,
|
||||
de,
|
||||
en,
|
||||
hu,
|
||||
lt,
|
||||
pl,
|
||||
ro,
|
||||
sk,
|
||||
};
|
||||
|
||||
function getName({ firstName, lastName }: { firstName?: string; lastName?: string }) {
|
||||
return [firstName, lastName].filter(Boolean).join(" ");
|
||||
}
|
||||
|
||||
function getTemplate(websiteLang: Lang, branch: CollectionEntry<"branches">): TemplateFn {
|
||||
// If website is in english (= international version), always use english translation of the letter
|
||||
// If using other language version of the website, use associated language of the branch as letter language
|
||||
const letterLang = websiteLang === "en" ? "en" : branch.data.lang;
|
||||
return templates[branch.data.messageTemplate ?? letterLang] ?? en;
|
||||
export function getTemplate(
|
||||
websiteLang: Lang,
|
||||
branch: CollectionEntry<"branches">
|
||||
): { render: TemplateFn; letterLang: Lang } {
|
||||
const { template: templateId } = branch.data;
|
||||
const template = templates[templateId];
|
||||
if (!template) {
|
||||
throw new Error(`Unknown template '${templateId}'`);
|
||||
}
|
||||
const { render, lang } = template;
|
||||
return { render, letterLang: lang };
|
||||
}
|
||||
|
||||
export async function renderText(
|
||||
websiteLang: Lang,
|
||||
{
|
||||
firstName,
|
||||
lastName,
|
||||
variant = "1",
|
||||
gender = "f",
|
||||
branch: branchId = "vig-holding",
|
||||
isClient = false,
|
||||
}: Partial<LetterOptions>
|
||||
) {
|
||||
const branch = await getEntry("branches", branchId);
|
||||
if (!branch) {
|
||||
throw new Error(`Unknown branch of id ${branchId}`);
|
||||
}
|
||||
|
||||
export function getMessage({
|
||||
websiteLang,
|
||||
options,
|
||||
selectedBranch,
|
||||
}: {
|
||||
websiteLang: Lang;
|
||||
options: Options;
|
||||
selectedBranch: CollectionEntry<"branches">;
|
||||
}) {
|
||||
const { render, letterLang } = getTemplate(websiteLang, selectedBranch);
|
||||
const { firstName, lastName, variant, gender, isClient } = options;
|
||||
const name = getName({ firstName, lastName });
|
||||
const template = getTemplate(websiteLang, branch);
|
||||
|
||||
return template({
|
||||
const message = render({
|
||||
name,
|
||||
variant,
|
||||
branch: branch.data.name,
|
||||
branchId,
|
||||
branchId: selectedBranch.id,
|
||||
branch: selectedBranch.data.name,
|
||||
gender,
|
||||
isClient,
|
||||
});
|
||||
return { letterLang, message };
|
||||
}
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import type { TemplateFn } from "./types";
|
||||
|
||||
/**
|
||||
* @status done
|
||||
*/
|
||||
export const lt: TemplateFn = (c) => {
|
||||
export const lang = "lt";
|
||||
|
||||
export const render: TemplateFn = (c) => {
|
||||
// prettier-ignore
|
||||
return (
|
||||
`Sveiki,
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import type { TemplateFn } from "./types";
|
||||
|
||||
/**
|
||||
* @status done
|
||||
*/
|
||||
export const pl: TemplateFn = (c) => {
|
||||
export const lang = "pl";
|
||||
|
||||
export const render: TemplateFn = (c) => {
|
||||
// prettier-ignore
|
||||
return (
|
||||
`Cześć!
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import type { TemplateFn } from "./types";
|
||||
|
||||
export const ro: TemplateFn = (c) => {
|
||||
export const lang = "ro";
|
||||
|
||||
export const render: TemplateFn = (c) => {
|
||||
// prettier-ignore
|
||||
return (
|
||||
`Bună ziua,
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import type { TemplateFn } from "./types";
|
||||
|
||||
/**
|
||||
* @status done
|
||||
*/
|
||||
export const sk: TemplateFn = (c) => {
|
||||
export const lang = "sk";
|
||||
|
||||
export const render: TemplateFn = (c) => {
|
||||
// prettier-ignore
|
||||
return (
|
||||
`Dobrý deň
|
||||
|
|
|
@ -1,3 +1,10 @@
|
|||
import type { Lang } from "../../lang";
|
||||
|
||||
export interface Template {
|
||||
lang: Lang;
|
||||
render: TemplateFn;
|
||||
}
|
||||
|
||||
export type TemplateFn = (context: TemplateContext) => string;
|
||||
|
||||
export type TemplateContext = {
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import type { TemplateFn } from "./types";
|
||||
|
||||
/**
|
||||
* @status done
|
||||
*/
|
||||
export const vigHolding: TemplateFn = (c) => {
|
||||
export const lang = "en";
|
||||
|
||||
export const render: TemplateFn = (c) => {
|
||||
// prettier-ignore
|
||||
return (
|
||||
`Dear M. Löger,
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
import { z } from "zod";
|
||||
|
||||
// Convert key presence or absence in form data to boolean
|
||||
export const checkbox = () => z.unknown().transform((value) => value !== undefined);
|
|
@ -1,14 +1,14 @@
|
|||
import "dotenv/config";
|
||||
import type { MiddlewareHandler } from "astro";
|
||||
import { isKnownLang } from "./lang";
|
||||
import { UnknownLanguageError } from "./lang";
|
||||
|
||||
export const onRequest: MiddlewareHandler = async ({ params }, next) => {
|
||||
const lang = params.lang;
|
||||
|
||||
// Unknown language returns 404
|
||||
if (lang && !isKnownLang(lang)) {
|
||||
return new Response(null, { status: 404, statusText: "Not found" });
|
||||
try {
|
||||
return await next();
|
||||
} catch (error) {
|
||||
if (error instanceof UnknownLanguageError) {
|
||||
return new Response(null, { status: 404, statusText: "Not found" });
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
return await next();
|
||||
};
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
---
|
||||
import { getEntry } from "astro:content";
|
||||
import Layout from "../../layouts/Layout.astro";
|
||||
import { getWebsiteLang } from "../../lang";
|
||||
|
||||
export { getStaticPaths } from "../../static-paths";
|
||||
|
||||
const { lang } = Astro.params;
|
||||
const websiteLang = getWebsiteLang(Astro.params);
|
||||
|
||||
const entry = (await getEntry("emailConfirmed", lang)) ?? (await getEntry("emailConfirmed", "en"));
|
||||
const entry =
|
||||
(await getEntry("emailConfirmed", websiteLang)) ?? (await getEntry("emailConfirmed", "en"));
|
||||
const { Content } = await entry.render();
|
||||
---
|
||||
|
||||
|
|
|
@ -1,16 +1,28 @@
|
|||
---
|
||||
import { Image } from "astro:assets";
|
||||
import { getEntry } from "astro:content";
|
||||
import Layout from "../../layouts/Layout.astro";
|
||||
import imgLogoVig from "../../assets/images/vig-logo.webp";
|
||||
import imgBanknotes from "../../assets/images/banknotes.webp";
|
||||
import { getEntry } from "astro:content";
|
||||
import LetterBuilder from "../../letter/LetterBuilder.astro";
|
||||
import { parseOptionsFromRequest } from "../../letter/request";
|
||||
import { getWebsiteLang } from "../../lang";
|
||||
|
||||
export { getStaticPaths } from "../../static-paths";
|
||||
|
||||
const { lang } = Astro.params;
|
||||
const websiteLang = getWebsiteLang(Astro.params);
|
||||
|
||||
const entry = (await getEntry("intro", lang)) ?? (await getEntry("intro", "en"));
|
||||
// Get options
|
||||
|
||||
const result = await parseOptionsFromRequest(Astro.url.searchParams);
|
||||
if (result.status === "notFound") {
|
||||
return new Response(null, { status: 404 });
|
||||
}
|
||||
const { options } = result;
|
||||
|
||||
// Get intro text
|
||||
|
||||
const entry = (await getEntry("intro", websiteLang)) ?? (await getEntry("intro", "en"));
|
||||
const { Content } = await entry.render();
|
||||
---
|
||||
|
||||
|
@ -47,10 +59,6 @@ const { Content } = await entry.render();
|
|||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="text-center text-3xl font-display font-bold mt-16">
|
||||
<p>Collection of letters has ended!</p>
|
||||
<p>Thank you</p>
|
||||
</div>
|
||||
<!-- <LetterBuilder lang={lang} /> -->
|
||||
<LetterBuilder {websiteLang} {options} />
|
||||
</div>
|
||||
</Layout>
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
---
|
||||
import SubmitResponseLayout from "../../letter/SubmitResponseLayout.astro";
|
||||
import { makeT } from "../../i18n";
|
||||
import { getWebsiteLang } from "../../lang";
|
||||
|
||||
export { getStaticPaths } from "../../static-paths";
|
||||
export const partial = true;
|
||||
|
||||
const { lang } = Astro.params;
|
||||
const { t } = makeT(lang);
|
||||
const websiteLang = getWebsiteLang(Astro.params);
|
||||
const { t } = makeT(websiteLang);
|
||||
---
|
||||
|
||||
<SubmitResponseLayout
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
---
|
||||
import SubmitResponseLayout from "../../letter/SubmitResponseLayout.astro";
|
||||
import { makeT } from "../../i18n";
|
||||
import { getWebsiteLang } from "../../lang";
|
||||
|
||||
export { getStaticPaths } from "../../static-paths";
|
||||
export const partial = true;
|
||||
|
||||
const { lang } = Astro.params;
|
||||
const { t } = makeT(lang);
|
||||
const websiteLang = getWebsiteLang(Astro.params);
|
||||
const { t } = makeT(websiteLang);
|
||||
---
|
||||
|
||||
<SubmitResponseLayout
|
||||
|
|
|
@ -1,28 +1,27 @@
|
|||
---
|
||||
import { z } from "zod";
|
||||
import type { Lang } from "../../lang";
|
||||
import { getEntry } from "astro:content";
|
||||
import { getWebsiteLang, type Lang } from "../../lang";
|
||||
import GenderInput from "../../letter/GenderInput.astro";
|
||||
import LetterBodyForm from "../../letter/LetterBodyForm.astro";
|
||||
import { checkbox } from "../../lib/types";
|
||||
import { parseOptionsFromRequest } from "../../letter/request";
|
||||
import { getMessage } from "../../letter/templates";
|
||||
import { getBranchOptions } from "../../letter/branches";
|
||||
|
||||
export const prerender = false;
|
||||
export const partial = true;
|
||||
|
||||
interface Params {
|
||||
lang: Lang;
|
||||
const websiteLang = getWebsiteLang(Astro.params);
|
||||
const result = await parseOptionsFromRequest(Astro.url.searchParams);
|
||||
|
||||
if (result.status === "notFound") {
|
||||
return new Response(null, { status: 404 });
|
||||
}
|
||||
|
||||
export const requestSchema = z.object({
|
||||
firstName: z.string().optional(),
|
||||
lastName: z.string().optional(),
|
||||
variant: z.string().optional(),
|
||||
gender: z.string().optional(),
|
||||
branch: z.string().optional(),
|
||||
isClient: checkbox(),
|
||||
});
|
||||
|
||||
const { lang } = Astro.params as unknown as Params;
|
||||
const params = Astro.url.searchParams;
|
||||
const options = requestSchema.parse(Object.fromEntries(params.entries()));
|
||||
const { options } = result;
|
||||
const branchOptions = await getBranchOptions(websiteLang);
|
||||
const selectedBranch = (await getEntry("branches", options.branchId ?? branchOptions[0].id))!;
|
||||
const { message, letterLang } = getMessage({ websiteLang, options, selectedBranch });
|
||||
---
|
||||
|
||||
<LetterBodyForm lang={lang} options={options} />
|
||||
<LetterBodyForm hx-swap-oob {websiteLang} {letterLang} {message} {options} />
|
||||
<GenderInput hx-swap-oob {websiteLang} {letterLang} {options} />
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import type { APIContext } from "astro";
|
||||
import type { Lang } from "../../lang";
|
||||
import { letterFormSchema } from "../../letter/schema";
|
||||
import { db } from "../../db";
|
||||
import * as schema from "../../schema";
|
||||
import { generateToken } from "../../utils";
|
||||
import { mailClient } from "../../mail";
|
||||
import { renderMail } from "../../mails/confirm-email";
|
||||
import { makeT } from "../../i18n";
|
||||
import { finalValidationSchema } from "../../letter/schema";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
|
||||
export const prerender = false;
|
||||
|
@ -16,9 +16,17 @@ interface Params extends Record<string, string> {
|
|||
}
|
||||
|
||||
export async function POST({ request, params: { lang }, redirect }: APIContext<never, Params>) {
|
||||
const formData = await request.formData();
|
||||
const validationResult = await letterFormSchema.safeParseAsync({
|
||||
...Object.fromEntries(formData.entries()),
|
||||
const form = await request.formData();
|
||||
const validationResult = await finalValidationSchema.safeParseAsync({
|
||||
firstName: form.get("firstName"),
|
||||
lastName: form.get("lastName"),
|
||||
variant: form.get("variant"),
|
||||
gender: form.get("gender"),
|
||||
branchId: form.get("branch"),
|
||||
isClient: form.has("isClient"),
|
||||
email: form.get("email"),
|
||||
message: form.get("message"),
|
||||
subscribed: form.has("subscribed"),
|
||||
});
|
||||
|
||||
if (!validationResult.success) {
|
||||
|
@ -31,7 +39,7 @@ export async function POST({ request, params: { lang }, redirect }: APIContext<n
|
|||
const { data } = validationResult;
|
||||
|
||||
const existingLetter = await db.query.letters.findFirst({
|
||||
where: and(eq(schema.letters.email, data.email), eq(schema.letters.branch, data.branch)),
|
||||
where: and(eq(schema.letters.email, data.email), eq(schema.letters.branch, data.branchId)),
|
||||
});
|
||||
|
||||
if (existingLetter) {
|
||||
|
@ -40,6 +48,7 @@ export async function POST({ request, params: { lang }, redirect }: APIContext<n
|
|||
|
||||
const values = {
|
||||
...data,
|
||||
branch: data.branchId,
|
||||
confirmationToken: generateToken(32),
|
||||
updated: new Date(),
|
||||
phone: data.phone ?? "",
|
||||
|
|
|
@ -3,11 +3,13 @@ import { Image } from "astro:assets";
|
|||
import { LANGUAGES } from "../config";
|
||||
import imgLogoVig from "../assets/images/vig-logo.webp";
|
||||
import Layout from "../layouts/Layout.astro";
|
||||
import { countries } from "../countries";
|
||||
import { getCollection } from "astro:content";
|
||||
import { getEntry } from "astro:content";
|
||||
|
||||
const localeVariants = LANGUAGES.map((lang) => ({ lang, country: countries[lang] })).toSorted(
|
||||
(a, b) => (b.lang === "en" ? Infinity : a.country.localeCompare(b.country)),
|
||||
const regions = (await getCollection("regions")).toSorted((a, b) =>
|
||||
b.id === "intl" ? Infinity : a.id.localeCompare(b.id)
|
||||
);
|
||||
const languages = await getCollection("languages");
|
||||
---
|
||||
|
||||
<Layout>
|
||||
|
@ -21,7 +23,7 @@ const localeVariants = LANGUAGES.map((lang) => ({ lang, country: countries[lang]
|
|||
<h2 class="mb-6 font-bold">Choose your country</h2>
|
||||
<ul class="grid md:grid-cols-2 gap-x-6 gap-y-4">
|
||||
{
|
||||
localeVariants.map(({ lang, country }) => (
|
||||
regions.map((region) => (
|
||||
<li class="flex gap-2">
|
||||
<svg
|
||||
class="w-6"
|
||||
|
@ -31,9 +33,15 @@ const localeVariants = LANGUAGES.map((lang) => ({ lang, country: countries[lang]
|
|||
>
|
||||
<path d="M16.1716 10.9999L10.8076 5.63589L12.2218 4.22168L20 11.9999L12.2218 19.778L10.8076 18.3638L16.1716 12.9999H4V10.9999H16.1716Z" />
|
||||
</svg>
|
||||
<a class="link" hreflang={lang} href={`/${lang}`}>
|
||||
{country}
|
||||
</a>
|
||||
<span>{region.data.title}</span>
|
||||
{region.data.languages.map((lang) => {
|
||||
const { title } = languages.find((l) => l.id === lang)?.data ?? {};
|
||||
return (
|
||||
<a class="link" hreflang={lang} href={`/${lang}`}>
|
||||
{title}
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</li>
|
||||
))
|
||||
}
|
||||
|
|
|
@ -27,4 +27,5 @@ export const messages = pgTable("messages", {
|
|||
from: text("from").notNull(),
|
||||
to: text("to").notNull(),
|
||||
status: statusEnum("status").notNull(),
|
||||
foo: text("foo"),
|
||||
});
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
---
|
||||
interface Props {
|
||||
import type { HTMLAttributes } from "astro/types";
|
||||
|
||||
interface Props extends HTMLAttributes<"fieldset"> {
|
||||
required?: boolean;
|
||||
label: string;
|
||||
}
|
||||
|
@ -7,7 +9,7 @@ interface Props {
|
|||
const { label, required = false, ...props } = Astro.props;
|
||||
---
|
||||
|
||||
<fieldset {...props}>
|
||||
<fieldset {...props} hx-swap-oob>
|
||||
<legend class="mb-3 text-base">
|
||||
{label}
|
||||
{required && <span class="required-tag">*</span>}
|
||||
|
|
|
@ -2,11 +2,11 @@
|
|||
import { Image } from "astro:assets";
|
||||
import imgSausages from "../../assets/images/spekacky.webp";
|
||||
import { makeT } from "../../i18n";
|
||||
import type { Lang } from "../../lang";
|
||||
import { tryGetWebsiteLang, type Lang } from "../../lang";
|
||||
import LogoDirtyMoney from "../logos/LogoDirtyMoney.astro";
|
||||
import { DEFAULT_LANG } from "../../config";
|
||||
|
||||
const { lang } = Astro.params;
|
||||
|
||||
const lang = tryGetWebsiteLang(Astro.params) ?? DEFAULT_LANG;
|
||||
const { t } = makeT(lang as Lang);
|
||||
---
|
||||
|
||||
|
|
Reference in a new issue