Skip to content

Commit 5923b21

Browse files
committed
Lagt til redigering av epostmaler
1 parent cf62a8e commit 5923b21

14 files changed

Lines changed: 1222 additions & 97 deletions

File tree

README.md

Lines changed: 146 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -87,15 +87,152 @@ src/
8787

8888
### Tabeller
8989

90-
| Tabell | Beskrivelse |
91-
|-----------------|-------------|
92-
| `events` | Arrangementer med dato, sted, registreringsperiode |
93-
| `courses` | Kurs innenfor et arrangement (aldersgruppe, kapasitet) |
94-
| `registrations` | Påmeldinger med status (confirmed/waitlisted/cancelled) |
95-
| `adminUsers` | Admin-brukere (opprettet via seed) |
96-
| `sessions` | Admin-sesjoner (cookie-basert) |
97-
| `siteContent` | Nøkkel/verdi-par for redigerbart sideinnhold |
98-
| `contactCards` | Kontaktkort for kontaktsiden |
90+
Drizzle-skjemaet ligger i `src/lib/server/db/schema.ts`. Alle `timestamp`-kolonner lagres uten tidssone, alle UUID-kolonner er `gen_random_uuid()`-defaulted der ikke annet er spesifisert.
91+
92+
#### `events`
93+
94+
Arrangementer — én rad per javaBin Kids-dag (f.eks. "javaBin Kids Vår 2026"). Kurs og forslag henger under et arrangement.
95+
96+
| Kolonne | Type | Beskrivelse |
97+
|---|---|---|
98+
| `arrangementId` | uuid, PK | Unik ID |
99+
| `title` | text | Visningstittel |
100+
| `description` | text | Markdown-beskrivelse |
101+
| `date` | timestamp | Dato for arrangementet |
102+
| `location` | text | Sted/adresse |
103+
| `cancelled` | boolean | Om arrangementet er avlyst |
104+
| `registrationOpens` | timestamp | Start for påmelding |
105+
| `registrationCloses` | timestamp | Slutt for påmelding |
106+
| `imageUrl` | text, nullable | URL til forsidebilde (fra `images`-tabellen eller ekstern) |
107+
| `openForSubmissions` | boolean | Om åpent for innsending av kursforslag |
108+
| `submissionDeadline` | timestamp, nullable | Frist for innsending av forslag |
109+
| `createdAt`, `updatedAt` | timestamp | Auto |
110+
111+
#### `courses`
112+
113+
Kurs som arrangeres innenfor et gitt arrangement. Hvert kurs har egen alders- og kapasitetsgrense.
114+
115+
| Kolonne | Type | Beskrivelse |
116+
|---|---|---|
117+
| `courseId` | uuid, PK | Unik ID |
118+
| `arrangementId` | uuid, FK → `events` | Hvilket arrangement kurset tilhører (`ON DELETE RESTRICT`) |
119+
| `title` | text | Kurstittel |
120+
| `introduction` | text | Kort introduksjonstekst |
121+
| `description` | text | Fullstendig Markdown-beskrivelse |
122+
| `thumbnailUrl` | text, nullable | URL til thumbnail |
123+
| `ageMin`, `ageMax` | integer | Aldersgruppe (inkluderende) |
124+
| `maxParticipants` | integer | Maks antall påmeldte før venteliste |
125+
| `createdAt`, `updatedAt` | timestamp | Auto |
126+
127+
#### `submissions`
128+
129+
Kursforslag sendt inn av eksterne foredragsholdere via forslagsskjemaet. Går gjennom admin-vurdering (`submitted``approved`/`rejected`).
130+
131+
| Kolonne | Type | Beskrivelse |
132+
|---|---|---|
133+
| `submissionId` | uuid, PK | Unik ID |
134+
| `arrangementId` | uuid, FK → `events` | Hvilket arrangement forslaget er sendt til |
135+
| `status` | text | `submitted`, `approved` eller `rejected` |
136+
| `title` | text | Kurstittel |
137+
| `description` | text | Markdown-beskrivelse |
138+
| `equipmentRequirements` | text, nullable | Utstyrsbehov |
139+
| `ageMin`, `ageMax` | integer | Foreslått aldersgruppe |
140+
| `maxParticipants` | integer | Foreslått maks antall |
141+
| `speakerName` | text | Foredragsholders navn |
142+
| `speakerEmail` | text | E-post (brukes til mottatt/godkjent/avvist-mail) |
143+
| `speakerBio` | text | Kort bio |
144+
| `editToken` | uuid | Token i redigeringslenke, lar foredragsholder endre forslaget uten innlogging |
145+
| `createdAt`, `updatedAt` | timestamp | Auto |
146+
147+
#### `registrations`
148+
149+
Påmeldinger fra foreldre til et spesifikt kurs. Unik på `(courseId, parentEmail, childName)` for å hindre duplikater.
150+
151+
| Kolonne | Type | Beskrivelse |
152+
|---|---|---|
153+
| `registrationId` | uuid, PK | Unik ID |
154+
| `courseId` | uuid, FK → `courses` | Kurset barnet meldes på |
155+
| `parentName` | text | Forelders navn |
156+
| `parentEmail` | text | E-post (brukes til bekreftelse/påminnelse) |
157+
| `parentPhone` | text | Telefonnummer |
158+
| `childName` | text | Barnets navn |
159+
| `childAge` | integer | Barnets alder |
160+
| `status` | text | `confirmed`, `waitlisted` eller `cancelled` |
161+
| `waitlistPosition` | integer, nullable | Posisjon på venteliste (1 = først i køen); `null` for confirmed/cancelled |
162+
| `consentGiven` | boolean | Om samtykke til databehandling er gitt |
163+
| `cancellationToken` | uuid | Token i bekreftelse/avmeldings-lenke — gjør at forelder kan avmelde uten innlogging |
164+
| `createdAt`, `updatedAt` | timestamp | Auto |
165+
166+
#### `adminUsers`
167+
168+
Admin-brukere som kan logge inn i admin-panelet. Opprettes via `npm run db:seed`.
169+
170+
| Kolonne | Type | Beskrivelse |
171+
|---|---|---|
172+
| `adminUserId` | uuid, PK | Unik ID |
173+
| `username` | text, unique | Brukernavn |
174+
| `passwordHash` | text | bcrypt-hash av passord |
175+
| `createdAt` | timestamp | Auto |
176+
177+
#### `sessions`
178+
179+
Aktive admin-sesjoner. Sesjons-ID-en lagres som HTTP-cookie og valideres på hver admin-request.
180+
181+
| Kolonne | Type | Beskrivelse |
182+
|---|---|---|
183+
| `sessionId` | uuid, PK | ID som lagres i cookien |
184+
| `adminUserId` | uuid, FK → `adminUsers` | Hvilken bruker sesjonen tilhører |
185+
| `expiresAt` | timestamp | Utløp (typisk 24 timer etter innlogging) |
186+
| `createdAt` | timestamp | Auto |
187+
188+
#### `siteContent`
189+
190+
Nøkkel/verdi-tekst som admin kan redigere fra `/admin/innhold`. Brukes for forside-hero, om-siden osv.
191+
192+
| Kolonne | Type | Beskrivelse |
193+
|---|---|---|
194+
| `key` | text, PK | Identifikator, f.eks. `hero_title`, `om_content` |
195+
| `content` | text | Fritekst/Markdown |
196+
| `updatedAt` | timestamp | Auto |
197+
198+
#### `images`
199+
200+
Binær-lagring av opplastede bilder (thumbnails, forsidebilder). Serveres via `/api/images/{imageId}` som bruker `mimeType` som `Content-Type`.
201+
202+
| Kolonne | Type | Beskrivelse |
203+
|---|---|---|
204+
| `imageId` | uuid, PK | Unik ID (del av serve-URL) |
205+
| `filename` | text | Opprinnelig filnavn |
206+
| `mimeType` | text | `image/jpeg`, `image/png`, osv. |
207+
| `data` | bytea | Rå bilde-bytes |
208+
| `createdAt` | timestamp | Auto |
209+
210+
#### `emailTemplates`
211+
212+
Redigerbare e-postmaler for de åtte transaksjonelle e-postene (bekreftelse, venteliste, forslag-godkjent, osv.). Hvis en rad mangler eller et felt er tomt, faller `email.ts` tilbake på hardkodet default. Tekstfelter kan inneholde `{{variabel}}`-plassholdere og `**fet tekst**`.
213+
214+
| Kolonne | Type | Beskrivelse |
215+
|---|---|---|
216+
| `templateKey` | text, PK | Én av `confirmation`, `waitlist`, `promotion`, `cancellation`, `reminder`, `submissionReceived`, `submissionApproved`, `submissionRejected` |
217+
| `subject` | text | Emnelinje |
218+
| `heading` | text | Overskrift i e-post-kroppen |
219+
| `introText` | text | Paragrafer før info-bokser (tom linje = ny paragraf) |
220+
| `outroText` | text | Paragrafer etter knapp/info-bokser |
221+
| `buttonText` | text, nullable | CTA-knappens tekst (kun for maler med knapp) |
222+
| `updatedAt` | timestamp | Auto |
223+
224+
#### `contactCards`
225+
226+
Kontaktkort som vises på kontaktsiden, administreres fra `/admin/innhold`.
227+
228+
| Kolonne | Type | Beskrivelse |
229+
|---|---|---|
230+
| `contactCardId` | uuid, PK | Unik ID |
231+
| `title` | text | Kort-tittel |
232+
| `actionType` | text | `email`, `link` eller `phone` |
233+
| `actionValue` | text | E-post, URL eller telefonnummer |
234+
| `sortOrder` | integer | Visningsrekkefølge (stigende) |
235+
| `createdAt`, `updatedAt` | timestamp | Auto |
99236

100237
### Migreringer
101238

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
CREATE TABLE IF NOT EXISTS "emailTemplates" (
2+
"templateKey" text PRIMARY KEY NOT NULL,
3+
"subject" text DEFAULT '' NOT NULL,
4+
"heading" text DEFAULT '' NOT NULL,
5+
"introText" text DEFAULT '' NOT NULL,
6+
"outroText" text DEFAULT '' NOT NULL,
7+
"buttonText" text,
8+
"updatedAt" timestamp DEFAULT now() NOT NULL
9+
);
10+
11+
INSERT INTO "emailTemplates" ("templateKey", "subject", "heading", "introText", "outroText", "buttonText") VALUES
12+
(
13+
'confirmation',
14+
'Påmelding bekreftet: {{courseTitle}}',
15+
'Hei {{parentName}}!',
16+
'**{{childName}}** er nå påmeldt **{{courseTitle}}**.',
17+
'',
18+
'Se bekreftelse'
19+
),
20+
(
21+
'waitlist',
22+
'Venteliste: {{courseTitle}}',
23+
'Hei {{parentName}}!',
24+
'**{{childName}}** er satt på venteliste for **{{courseTitle}}**.',
25+
'Du vil motta e-post dersom en plass blir ledig.',
26+
'Se status'
27+
),
28+
(
29+
'promotion',
30+
'Plass ledig: {{courseTitle}}',
31+
'Gode nyheter, {{parentName}}!',
32+
'En plass har blitt ledig, og **{{childName}}** er nå bekreftet påmeldt **{{courseTitle}}**.',
33+
'Vi gleder oss til å se dere!',
34+
NULL
35+
),
36+
(
37+
'cancellation',
38+
'Avmelding bekreftet: {{courseTitle}}',
39+
'Hei {{parentName}}!',
40+
'Påmeldingen for **{{childName}}** til **{{courseTitle}}** er nå avbestilt.',
41+
'Dersom dette var en feil, kan du melde deg på igjen via nettsiden.',
42+
NULL
43+
),
44+
(
45+
'reminder',
46+
'Påminnelse: {{courseTitle}} nærmer seg!',
47+
'Hei {{parentName}}!',
48+
'Vi minner om at **{{childName}}** er påmeldt **{{courseTitle}}**.',
49+
'Vi gleder oss til å se dere!',
50+
NULL
51+
),
52+
(
53+
'submissionReceived',
54+
'Forslag mottatt: {{title}}',
55+
'Hei {{speakerName}}!',
56+
E'Vi har mottatt forslaget ditt **{{title}}** til **{{eventTitle}}**.\n\nDu kan redigere forslaget ditt frem til **{{submissionDeadline}}**.',
57+
'Du vil få beskjed på e-post når forslaget er vurdert.',
58+
'Rediger forslaget'
59+
),
60+
(
61+
'submissionApproved',
62+
'Forslag godkjent: {{title}}',
63+
'Gratulerer, {{speakerName}}!',
64+
'Forslaget ditt **{{title}}** er godkjent og blir med på **{{eventTitle}}**.',
65+
'Vi gleder oss til å se deg!',
66+
NULL
67+
),
68+
(
69+
'submissionRejected',
70+
'Forslag ikke tatt med: {{title}}',
71+
'Hei {{speakerName}}',
72+
'Takk for forslaget ditt **{{title}}** til **{{eventTitle}}**.',
73+
'Dessverre har vi ikke mulighet til å ta med dette forslaget denne gangen. Vi håper du vil sende inn forslag igjen ved en senere anledning!',
74+
NULL
75+
);

drizzle/migrations/meta/_journal.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,13 @@
4343
"when": 1774258800000,
4444
"tag": "0005_add_submissions",
4545
"breakpoints": true
46+
},
47+
{
48+
"idx": 6,
49+
"version": "7",
50+
"when": 1776409200000,
51+
"tag": "0006_add_email_templates",
52+
"breakpoints": true
4653
}
4754
]
4855
}

src/lib/server/db/schema.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,16 @@ export const images = pgTable('images', {
100100
createdAt: timestamp().notNull().defaultNow()
101101
});
102102

103+
export const emailTemplates = pgTable('emailTemplates', {
104+
templateKey: text().primaryKey(),
105+
subject: text().notNull().default(''),
106+
heading: text().notNull().default(''),
107+
introText: text().notNull().default(''),
108+
outroText: text().notNull().default(''),
109+
buttonText: text(),
110+
updatedAt: timestamp().notNull().defaultNow().$onUpdate(() => new Date())
111+
});
112+
103113
export const contactCards = pgTable('contactCards', {
104114
contactCardId: uuid().primaryKey().defaultRandom(),
105115
title: text().notNull(),

0 commit comments

Comments
 (0)