Skip to content

Commit cb9eceb

Browse files
committed
Legger ved mail logo, og formaterer eposter etter mal
1 parent f8b77e0 commit cb9eceb

6 files changed

Lines changed: 46 additions & 48 deletions

File tree

.env.example

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
DATABASE_URL=postgres://javabin:javabin@localhost:5432/javabinkids
2-
RESEND_API_KEY=re_your_api_key
2+
SENDGRID_API_KEY=SG.your_api_key
33
ADMIN_USERNAME=admin
44
ADMIN_PASSWORD=changeme
55
BASE_URL=http://localhost:5175
6+
# Prepend til epost subject. Brukes kun i DEV og TEST for å luke bort ikke-prod eposter.
7+
# Ta bort/Ikke sett denne i prod for å ikke ha med prefix i epost subject i prod.
8+
EMAIL_SUBJECT_PREFIX=[DEV]

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Nettsiden presenterer arrangementer, håndterer påmelding med venteliste, og ha
1212
| Backend | SvelteKit server routes (`+page.server.ts`, `+server.ts`) |
1313
| Database | PostgreSQL 17 via [Drizzle ORM](https://orm.drizzle.team) |
1414
| Validering | [Zod](https://zod.dev) |
15-
| E-post | [Resend](https://resend.com) |
15+
| E-post | [SendGrid](https://sendgrid.com) |
1616
| Markdown | [marked](https://marked.js.org) (kursbeskrivelser) |
1717
| Auth | Cookie-basert sesjoner med bcrypt |
1818
| Infra | Docker Compose (dev + backup) |
@@ -57,10 +57,11 @@ Se `.env.example`:
5757
| Variabel | Beskrivelse |
5858
|----------|-------------|
5959
| `DATABASE_URL` | PostgreSQL connection string |
60-
| `RESEND_API_KEY` | API-nøkkel fra [resend.com](https://resend.com) |
60+
| `SENDGRID_API_KEY` | API-nøkkel fra [sendgrid.com](https://sendgrid.com) |
6161
| `ADMIN_USERNAME` | Brukernavn for admin (brukes av seed) |
6262
| `ADMIN_PASSWORD` | Passord for admin (brukes av seed) |
6363
| `BASE_URL` | Offentlig URL for e-postlenker |
64+
| `EMAIL_SUBJECT_PREFIX` | (Valgfritt) Prefix som legges til alle e-post-emnelinjer. Nyttig for å skille test-e-poster fra produksjon, f.eks. `[DEV] `. |
6465

6566
## Prosjektstruktur
6667

@@ -213,7 +214,7 @@ Prosjektet inkluderer en `Dockerfile` for produksjonsbygg med multi-stage build:
213214
docker build -t javabinkids .
214215
docker run -p 3000:3000 \
215216
-e DATABASE_URL=postgres://... \
216-
-e RESEND_API_KEY=re_... \
217+
-e SENDGRID_API_KEY=SG.... \
217218
-e BASE_URL=https://kids.javabin.no \
218219
javabinkids
219220
```

docker-compose.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ services:
3232
ADMIN_USERNAME: ${ADMIN_USERNAME:-admin}
3333
ADMIN_PASSWORD: ${ADMIN_PASSWORD:-changeme}
3434
BASE_URL: http://localhost:5175
35+
EMAIL_SUBJECT_PREFIX: ${EMAIL_SUBJECT_PREFIX:-}
3536
depends_on:
3637
db:
3738
condition: service_healthy

src/lib/server/email.ts

Lines changed: 36 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -10,35 +10,37 @@ function getSendGrid() {
1010
}
1111

1212
async function sendEmail(params: { to: string; subject: string; html: string }) {
13+
const subjectPrefix = process.env.EMAIL_SUBJECT_PREFIX ?? '';
1314
await getSendGrid().send({
1415
from: FROM_EMAIL,
1516
to: params.to,
16-
subject: params.subject,
17+
subject: `${subjectPrefix}${params.subject}`,
1718
html: params.html
1819
});
1920
}
2021

21-
const FROM_EMAIL = 'javaBin Kids <kids@javabin.no>';
22+
const FROM_EMAIL = 'javaBin <kids@javabin.no>';
2223
const BASE_URL = process.env.BASE_URL || 'http://localhost:5175';
2324

2425
function emailLayout(content: string): string {
2526
return `
2627
<!DOCTYPE html>
2728
<html>
2829
<head><meta charset="utf-8" /><meta name="viewport" content="width=device-width" /></head>
29-
<body style="margin:0;padding:0;background-color:#0d1b2a;font-family:Arial,Helvetica,sans-serif;">
30-
<table width="100%" cellpadding="0" cellspacing="0" style="background-color:#0d1b2a;padding:40px 20px;">
30+
<body style="margin:0;padding:0;background-color:#F5F5F5;font-family:Arial,Helvetica,sans-serif;">
31+
<table width="100%" cellpadding="0" cellspacing="0" style="background-color:#F5F5F5;padding:40px 20px;">
3132
<tr><td align="center">
3233
<table width="600" cellpadding="0" cellspacing="0" style="max-width:600px;width:100%;">
33-
<tr><td style="text-align:center;padding:20px 0;">
34-
<span style="font-size:24px;font-weight:bold;color:#7ec8c8;">javaBin Kids</span>
34+
<tr><td style="background-color:#E74C3C;padding:32px 20px;text-align:center;border-radius:8px 8px 0 0;">
35+
<img src="${BASE_URL}/javabin-logo.png" alt="javaBin" style="max-width:200px;height:auto;display:inline-block;" />
3536
</td></tr>
36-
<tr><td style="background-color:#1a2f3a;border-radius:16px;padding:32px;border:1px solid rgba(255,255,255,0.1);">
37+
<tr><td style="background-color:#FFFFFF;padding:32px;border-radius:0 0 8px 8px;">
3738
${content}
3839
</td></tr>
39-
<tr><td style="text-align:center;padding:20px 0;color:#a0b4c0;font-size:12px;">
40-
<a href="https://java.no" style="color:#a0b4c0;text-decoration:none;">javaBin</a> ·
41-
<a href="https://java.no/principles" style="color:#a0b4c0;text-decoration:none;">Code of Conduct</a>
40+
<tr><td style="text-align:center;padding:20px 0;color:#6C757D;font-size:12px;line-height:1.6;">
41+
JavaBin — Javabrukerforeningen i Norge<br />
42+
<a href="https://java.no" style="color:#6C757D;text-decoration:none;">java.no</a> ·
43+
<a href="https://javazone.no" style="color:#6C757D;text-decoration:none;">javazone.no</a>
4244
</td></tr>
4345
</table>
4446
</td></tr>
@@ -48,23 +50,22 @@ function emailLayout(content: string): string {
4850
}
4951

5052
function heading(text: string): string {
51-
return `<h1 style="color:#7ec8c8;font-size:22px;margin:0 0 16px 0;">${text}</h1>`;
53+
return `<h1 style="color:#E74C3C;font-size:20px;font-weight:bold;margin:0 0 16px 0;">${text}</h1>`;
5254
}
5355

5456
function paragraph(text: string): string {
55-
return `<p style="color:#e8e8e8;font-size:15px;line-height:1.6;margin:0 0 12px 0;">${text}</p>`;
57+
return `<p style="color:#2D3748;font-size:15px;line-height:1.6;margin:0 0 12px 0;">${text}</p>`;
5658
}
5759

5860
function button(text: string, url: string): string {
59-
return `<p style="text-align:center;margin:24px 0 8px;"><a href="${url}" style="display:inline-block;background-color:#d4a843;color:#0d1b2a;font-weight:bold;font-size:15px;padding:12px 32px;border-radius:12px;text-decoration:none;">${text}</a></p>`;
61+
return `<p style="text-align:center;margin:24px 0 8px;"><a href="${url}" style="display:inline-block;background-color:#E74C3C;color:#FFFFFF;font-weight:bold;font-size:15px;padding:12px 32px;border-radius:12px;text-decoration:none;">${text}</a></p>`;
6062
}
6163

62-
function info(label: string, value: string): string {
63-
return `<tr><td style="color:#a0b4c0;padding:6px 0;font-size:14px;">${label}</td><td style="color:#e8e8e8;padding:6px 0;font-size:14px;text-align:right;font-weight:bold;">${value}</td></tr>`;
64-
}
65-
66-
function infoTable(rows: string): string {
67-
return `<table width="100%" cellpadding="0" cellspacing="0" style="margin:16px 0;border-top:1px solid rgba(255,255,255,0.1);border-bottom:1px solid rgba(255,255,255,0.1);">${rows}</table>`;
64+
function infoBox(label: string, value: string): string {
65+
return `<div style="background-color:#FBEEEE;padding:16px;border-radius:8px;margin:0 0 12px 0;">
66+
<div style="color:#6C757D;font-size:11px;font-weight:bold;text-transform:uppercase;letter-spacing:0.5px;margin-bottom:4px;">${label}</div>
67+
<div style="color:#2D3748;font-size:16px;font-weight:bold;">${value}</div>
68+
</div>`;
6869
}
6970

7071
export async function sendConfirmationEmail(params: {
@@ -86,13 +87,11 @@ export async function sendConfirmationEmail(params: {
8687
html: emailLayout(`
8788
${heading(`Hei ${params.parentName}!`)}
8889
${paragraph(`<strong>${params.childName}</strong> er nå påmeldt <strong>${params.courseTitle}</strong>.`)}
89-
${infoTable(`
90-
${info('Arrangement', params.eventTitle)}
91-
${info('Dato', params.eventDate)}
92-
${info('Kurs', params.courseTitle)}
93-
`)}
90+
${infoBox('Arrangement', params.eventTitle)}
91+
${infoBox('Dato', params.eventDate)}
92+
${infoBox('Kurs', params.courseTitle)}
9493
${button('Se bekreftelse', confirmUrl)}
95-
${paragraph(`<a href="${cancelUrl}" style="color:#a0b4c0;font-size:13px;">Avmeld</a>`)}
94+
${paragraph(`<a href="${cancelUrl}" style="color:#6C757D;font-size:13px;">Avmeld</a>`)}
9695
`)
9796
});
9897
}
@@ -115,10 +114,10 @@ export async function sendWaitlistEmail(params: {
115114
html: emailLayout(`
116115
${heading(`Hei ${params.parentName}!`)}
117116
${paragraph(`<strong>${params.childName}</strong> er satt på venteliste for <strong>${params.courseTitle}</strong>.`)}
118-
${paragraph(`Posisjon på venteliste: <strong style="color:#f0a830;">${params.position}</strong>`)}
117+
${infoBox('Posisjon på venteliste', String(params.position))}
119118
${paragraph('Du vil motta e-post dersom en plass blir ledig.')}
120119
${button('Se status', confirmUrl)}
121-
${paragraph(`<a href="${cancelUrl}" style="color:#a0b4c0;font-size:13px;">Avmeld</a>`)}
120+
${paragraph(`<a href="${cancelUrl}" style="color:#6C757D;font-size:13px;">Avmeld</a>`)}
122121
`)
123122
});
124123
}
@@ -137,11 +136,9 @@ export async function sendPromotionEmail(params: {
137136
html: emailLayout(`
138137
${heading(`Gode nyheter, ${params.parentName}!`)}
139138
${paragraph(`En plass har blitt ledig, og <strong>${params.childName}</strong> er nå bekreftet påmeldt <strong>${params.courseTitle}</strong>.`)}
140-
${infoTable(`
141-
${info('Arrangement', params.eventTitle)}
142-
${info('Dato', params.eventDate)}
143-
${info('Kurs', params.courseTitle)}
144-
`)}
139+
${infoBox('Arrangement', params.eventTitle)}
140+
${infoBox('Dato', params.eventDate)}
141+
${infoBox('Kurs', params.courseTitle)}
145142
${paragraph('Vi gleder oss til å se dere!')}
146143
`)
147144
});
@@ -179,12 +176,10 @@ export async function sendReminderEmail(params: {
179176
html: emailLayout(`
180177
${heading(`Hei ${params.parentName}!`)}
181178
${paragraph(`Vi minner om at <strong>${params.childName}</strong> er påmeldt <strong>${params.courseTitle}</strong>.`)}
182-
${infoTable(`
183-
${info('Arrangement', params.eventTitle)}
184-
${info('Dato', params.eventDate)}
185-
${info('Sted', params.eventLocation)}
186-
${info('Kurs', params.courseTitle)}
187-
`)}
179+
${infoBox('Arrangement', params.eventTitle)}
180+
${infoBox('Dato', params.eventDate)}
181+
${infoBox('Sted', params.eventLocation)}
182+
${infoBox('Kurs', params.courseTitle)}
188183
${paragraph('Vi gleder oss til å se dere!')}
189184
`)
190185
});
@@ -228,11 +223,9 @@ export async function sendSubmissionApprovedEmail(params: {
228223
html: emailLayout(`
229224
${heading(`Gratulerer, ${params.speakerName}!`)}
230225
${paragraph(`Forslaget ditt <strong>${params.title}</strong> er godkjent og blir med på <strong>${params.eventTitle}</strong>.`)}
231-
${infoTable(`
232-
${info('Arrangement', params.eventTitle)}
233-
${info('Dato', params.eventDate)}
234-
${info('Kurs', params.title)}
235-
`)}
226+
${infoBox('Arrangement', params.eventTitle)}
227+
${infoBox('Dato', params.eventDate)}
228+
${infoBox('Kurs', params.title)}
236229
${paragraph('Vi gleder oss til å se deg!')}
237230
`)
238231
});

src/routes/personvern/+page.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
<h2>Deling</h2>
4545
<p>Vi deler ikke personopplysninger med tredjeparter, med unntak av:</p>
4646
<ul>
47-
<li>Resend (e-posttjeneste) for utsendelse av bekreftelser og påminnelser</li>
47+
<li>SendGrid (e-posttjeneste) for utsendelse av bekreftelser og påminnelser</li>
4848
</ul>
4949
</section>
5050

static/javabin-logo.png

4.97 KB
Loading

0 commit comments

Comments
 (0)