Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions apps/www/src/lib/components/app/header/Header.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,11 @@
<HeaderItem to="/" name="/hjem" />
<HeaderItem to="/meny" name="/meny" />
<HeaderItem to="https://forms.gle/BLdygdoRJgjMbQZj6" name="/booking" />
<HeaderItem to="/om-oss" name="/om oss" />
<HeaderItem to="/om-oss" name="/om_oss" />
{#if !!$user}
<HeaderSignOut />
{:else}
<HeaderItem to="/bli-frivillig" name="/bli_frivillig" />
{/if}
</ul>

Expand All @@ -74,11 +76,11 @@
<MenuItem to="/" name="/hjem" />
<MenuItem to="/meny" name="/meny" />
<MenuItem to="https://forms.gle/BLdygdoRJgjMbQZj6" name="/booking" />
<MenuItem to="/om-oss" name="/om oss" />
<MenuItem to="/om-oss" name="/om_oss" />
{#if !!$user}
<MenuSignOut />
{:else}
<MenuItem to="/logg-inn" name="/logg inn" />
<MenuItem to="/bli-frivillig" name="/bli_frivillig" />
{/if}
</ul>
{/if}
Expand Down
27 changes: 24 additions & 3 deletions apps/www/src/lib/services/email.service.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ export type InvitationEmailProps = {
email: string;
};

export type VolunteerRequestEmailProps = {
name: string;
email: string;
};

export type ShiftEmailProps = {
shift: {
id: string;
Expand All @@ -37,11 +42,10 @@ function generateICS(shift: {
summary: string;
description?: string;
}): string {
// Create a consistent UID based on event details
const uid = shift.id;
const uid = `${shift.startAt}-${shift.endAt}-${shift.summary.replace(/\s+/g, '-')}@programmerbar.no`;

const formatDate = (dateStr: string) => {
const date = new Date(dateStr);
// Convert to local time string in YYYYMMDDTHHMMSS format
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
Expand Down Expand Up @@ -93,6 +97,23 @@ export class EmailService {
});
}

async sendVolunteerRequestEmail(data: VolunteerRequestEmailProps) {
await this.sendEmail({
from: FROM_EMAIL,
subject: 'Ny frivillig-søknad',
to: ['frivilligansvarlig@programmerbar.no'],
html: `
<h1>Ny frivillig-søknad</h1>
<p>En ny person har søkt om å bli frivillig hos Programmerbar:</p>
<ul>
<li><strong>Navn:</strong> ${data.name}</li>
<li><strong>E-post:</strong> ${data.email}</li>
</ul>
<p>Brukeren har blitt lagt til i databasen og kan nå logge inn med Feide.</p>
`
});
}

async sendShiftEmail(data: ShiftEmailProps) {
const icsContent = generateICS(data.shift);

Expand Down
10 changes: 10 additions & 0 deletions apps/www/src/routes/(app)/bli-frivillig/+page.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// /routes/(app)/bli-frivillig/+page.server.ts
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';

export const load: PageServerLoad = async ({ locals }) => {
if (locals.user) {
throw redirect(302, '/portal');
}
return {};
};
125 changes: 125 additions & 0 deletions apps/www/src/routes/(app)/bli-frivillig/+page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
<script lang="ts">
import type { PageData } from './$types';
import { toast } from 'svelte-sonner';

const props = $props<{ data: PageData }>();

let name = $state('');
let email = $state('');
let isSubmitting = $state(false);
let error = $state('');

async function handleSubmit(event: SubmitEvent) {
event.preventDefault();

if (!name || !email) {
error = 'Vennligst fyll ut både navn og e-post';
return;
}

if (!email.endsWith('@student.uib.no')) {
error = 'E-post må være en student-e-post (@student.uib.no)';
return;
}

isSubmitting = true;
error = '';

try {
const response = await fetch('/api/volunteer-request', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ name, email })
});

if (!response.ok) {
const data = (await response.json()) as { error?: string };
error = data.error || 'Noe gikk galt. Vennligst pr√∏v igjen senere.';
return;
}

toast.success('Din s√∏knad er mottatt!');
name = '';
email = '';
} catch (err) {
console.error('Error submitting volunteer request:', err);
error = 'Noe gikk galt. Vennligst pr√∏v igjen senere.';
} finally {
isSubmitting = false;
}
}
</script>

<div class="mx-auto max-w-[600px] space-y-8 rounded-xl border-2 bg-background p-8">
<div class="space-y-4">
<h1 class="text-center text-3xl font-bold">Bli frivillig i Programmerbar</h1>
<p class="text-center text-gray-600">
Som frivillig i Programmerbar får du mulighet til å være med på å skape en fantastisk
studentpub-miljø for informatikkstudenter.
</p>
</div>
<div class="space-y-4">
<h2 class="text-xl font-semibold">Hva får du som frivillig?</h2>
<ul class="list-inside list-disc space-y-2 text-gray-600">
<li>En bong for hver vakt du jobber</li>
<li>Mulighet til å møte andre informatikkstudenter</li>
<li>Erfaring med drift av studentpub</li>
<li>Innblikk i hvordan en studentorganisasjon fungerer</li>
</ul>
</div>
<div class="space-y-4">
<h2 class="text-xl font-semibold">Hva forventes av deg?</h2>
<ul class="list-inside list-disc space-y-2 text-gray-600">
<li>Jobbe minst en vakt i måneden</li>
<li>Være ansvarlig og pålitelig</li>
<li>Ha god tid til å møte opp på vakter du har meldt deg på</li>
</ul>
</div>
{#if error}
<div class="rounded-md bg-red-50 p-4 text-red-700">
<p>{error}</p>
</div>
{/if}
<form onsubmit={handleSubmit} class="space-y-4">
<div class="space-y-2">
<label for="name" class="block text-sm font-medium">Navn</label>
<input
type="text"
id="name"
bind:value={name}
class="w-full rounded-lg border border-gray-300 p-2"
placeholder="Ditt fulle navn"
required
/>
</div>
<div class="space-y-2">
<label for="email" class="block text-sm font-medium">Student-e-post</label>
<input
type="email"
id="email"
bind:value={email}
class="w-full rounded-lg border border-gray-300 p-2"
placeholder="fornavn.etternavn@student.uib.no"
required
/>
<p class="text-xs text-gray-500">Du må bruke din student-e-post fra UiB</p>
</div>
<button
type="submit"
disabled={isSubmitting}
class="w-full rounded-lg border-2 border-primary bg-primary p-4 text-center text-lg font-medium text-white transition-colors hover:bg-primary-dark disabled:opacity-70"
>
{isSubmitting ? 'Sender...' : 'Send søknad'}
</button>
</form>
<div class="text-center">
<p class="mb-2">Har du allerede ein konto?</p>
<a
href="/auth/feide"
class="inline-block rounded-md bg-primary px-4 py-2 text-white transition-colors hover:bg-primary-dark"
>Logg inn</a
>
</div>
</div>
59 changes: 59 additions & 0 deletions apps/www/src/routes/api/volunteer-request/+server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { nanoid } from 'nanoid';
import type { RequestHandler } from './$types';
import { users } from '$lib/db/schemas';

export const POST: RequestHandler = async ({ request, locals }) => {
try {
const body = await request.json();
const { name, email } = body as { name: string; email: string };

if (!email.endsWith('@student.uib.no')) {
return new Response(
JSON.stringify({ error: 'Email must be a student email (@student.uib.no)' }),
{
status: 400,
headers: { 'Content-Type': 'application/json' }
}
);
}

const existingUser = await locals.db.query.users.findFirst({
where: (row, { eq }) => eq(row.email, email.toLowerCase())
});

if (existingUser) {
return new Response(JSON.stringify({ error: 'User already exists' }), {
status: 400,
headers: { 'Content-Type': 'application/json' }
});
}

const userId = nanoid();
const newUser = await locals.db
.insert(users)
.values({
id: userId,
name,
email: email.toLowerCase(),
role: 'normal'
})
.returning()
.then((rows) => rows[0]);

await locals.emailService.sendVolunteerRequestEmail({
name,
email
});

return new Response(JSON.stringify({ success: true, userId: newUser.id }), {
status: 201,
headers: { 'Content-Type': 'application/json' }
});
} catch (error) {
console.error('Error processing volunteer request:', error);
return new Response(JSON.stringify({ error: 'Internal server error' }), {
status: 500,
headers: { 'Content-Type': 'application/json' }
});
}
};