Move creator into events structure
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
export let authorized = false;
|
||||
|
||||
let refreshToken = '';
|
||||
let loading = true;
|
||||
|
||||
let to = '';
|
||||
let subject = '';
|
||||
let body = '';
|
||||
|
||||
async function validateToken(token: string): Promise<boolean> {
|
||||
if (!token) return false;
|
||||
const res = await fetch('/private/api/gmail', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ action: 'validate', refreshToken: token })
|
||||
});
|
||||
if (!res.ok) return false;
|
||||
const data = await res.json();
|
||||
return !!data.valid;
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
refreshToken = localStorage.getItem('gmail_refresh_token') ?? '';
|
||||
loading = true;
|
||||
authorized = await validateToken(refreshToken);
|
||||
loading = false;
|
||||
});
|
||||
|
||||
/* ⇢ redirects straight to Google via server 302 */
|
||||
const connect = () => goto('/private/api/gmail?action=auth');
|
||||
|
||||
async function disconnect() {
|
||||
if (!confirm('Disconnect Google account?')) return;
|
||||
await fetch('/private/api/gmail', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ action: 'revoke', refreshToken })
|
||||
});
|
||||
localStorage.removeItem('gmail_refresh_token');
|
||||
refreshToken = '';
|
||||
authorized = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="mb-4 rounded border border-gray-300 bg-white p-4">
|
||||
{#if loading}
|
||||
<div class="flex items-center space-x-2">
|
||||
<svg class="animate-spin h-5 w-5 text-gray-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8z"></path>
|
||||
</svg>
|
||||
<span>Checking Google connection...</span>
|
||||
</div>
|
||||
{:else}
|
||||
{#if !authorized}
|
||||
<section class="flex items-center justify-between w-full">
|
||||
<p class="mr-4">You haven’t connected your Google account yet.</p>
|
||||
<button class="btn bg-blue-600 hover:bg-blue-700 text-white font-semibold py-2 px-4 rounded ml-auto" on:click={connect}>
|
||||
Connect Google
|
||||
</button>
|
||||
</section>
|
||||
{:else}
|
||||
<div class="flex items-center space-x-2 text-green-600">
|
||||
<svg class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
<span>Your connection to Google is good, proceed to next step</span>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,25 @@
|
||||
<script lang="ts">
|
||||
export let email: { subject: string, body: string } = { subject: '', body: '' };
|
||||
</script>
|
||||
|
||||
<form class="flex flex-col space-y-4 bg-white p-8 rounded border border-gray-300 w-full shadow-none">
|
||||
<h2 class="text-2xl font-semibold text-center mb-4">Craft Email</h2>
|
||||
<label class="flex flex-col text-gray-700">
|
||||
Subject
|
||||
<input
|
||||
type="text"
|
||||
bind:value={email.subject}
|
||||
class="mt-1 px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-200"
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<label class="flex flex-col text-gray-700">
|
||||
Body
|
||||
<textarea
|
||||
bind:value={email.body}
|
||||
class="mt-1 px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-200 resize-none"
|
||||
rows="6"
|
||||
required
|
||||
></textarea>
|
||||
</label>
|
||||
</form>
|
||||
@@ -0,0 +1,71 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
|
||||
let { event } = $props();
|
||||
let loading = $state(false);
|
||||
|
||||
function handleEnhance() {
|
||||
loading = true;
|
||||
|
||||
return async ({ update }) => {
|
||||
await update();
|
||||
loading = false;
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<form method="POST" action="?/create" use:enhance={handleEnhance} class="flex flex-col space-y-4 bg-white p-8 rounded border border-gray-300 w-full shadow-none">
|
||||
<h2 class="text-2xl font-semibold text-center mb-4">Create Event</h2>
|
||||
<label class="flex flex-col text-gray-700">
|
||||
Name
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
class="mt-1 px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-200"
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<label class="flex flex-col text-gray-700">
|
||||
Date
|
||||
<input
|
||||
type="date"
|
||||
name="date"
|
||||
class="mt-1 px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-200"
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<label class="flex flex-col text-gray-700">
|
||||
Description
|
||||
<textarea
|
||||
name="description"
|
||||
class="mt-1 px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-200 resize-none"
|
||||
rows="3"
|
||||
required
|
||||
></textarea>
|
||||
</label>
|
||||
<button
|
||||
type="submit"
|
||||
class="w-full py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition"
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{#if Object.keys(event).length === 0}
|
||||
<div class="mt-4 rounded border-l-4 border-gray-500 bg-gray-100 p-4 text-gray-700">
|
||||
{#if loading}
|
||||
<strong>Loading...</strong>
|
||||
{:else}
|
||||
<strong>No event created yet...</strong>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="rounded border-l-4 border-green-500 bg-green-100 p-4 text-green-700 mt-4">
|
||||
<ol>
|
||||
<li><strong>{event.name}</strong></li>
|
||||
<li>{event.date}</li>
|
||||
<li>{event.description}</li>
|
||||
</ol>
|
||||
</div>
|
||||
{/if}
|
||||
59
src/routes/private/events/creator/steps/StepFinal.svelte
Normal file
59
src/routes/private/events/creator/steps/StepFinal.svelte
Normal file
@@ -0,0 +1,59 @@
|
||||
<script lang="ts">
|
||||
import QRCode from 'qrcode';
|
||||
|
||||
const StepState = {
|
||||
Waiting: 'waiting',
|
||||
Processing: 'processing',
|
||||
FinishedSuccess: 'finished_success',
|
||||
FinishedFail: 'finished_fail'
|
||||
};
|
||||
|
||||
let qr_codes_state = $state(StepState.Processing);
|
||||
let emails_state = $state(StepState.FinishedSuccess);
|
||||
|
||||
// Inserts all participants into the database and returns their assigned IDs.
|
||||
async function insert_data_supabase(data, participants, new_event) {
|
||||
const names = participants.map((p) => p.name);
|
||||
const surnames = participants.map((p) => p.surname);
|
||||
const emails = participants.map((p) => p.email);
|
||||
const {
|
||||
data: { user },
|
||||
error: authError
|
||||
} = await data.supabase.auth.getUser();
|
||||
const { data: user_profile, error: profileError } = await data.supabase
|
||||
.from('profiles')
|
||||
.select('*, section:sections (id, name)')
|
||||
.eq('id', user?.id)
|
||||
.single();
|
||||
const { data: result, error: qrCodeError } = await data.supabase.rpc('create_qrcodes_bulk', {
|
||||
p_section_id: user_profile?.section.id,
|
||||
p_event_id: new_event.id,
|
||||
p_names: names,
|
||||
p_surnames: surnames,
|
||||
p_emails: emails
|
||||
});
|
||||
|
||||
return { result };
|
||||
}
|
||||
|
||||
// Creates a base64 interpretation of the ticket ID
|
||||
function createB64QRCode(data) {
|
||||
QRCode.toDataURL('I am a pony!')
|
||||
.then((url) => {
|
||||
const parts = url.split(',');
|
||||
return { base64data: parts[1] };
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
|
||||
function sendEmail(email, subject, body, qr_code_base64) {
|
||||
// Here you would implement the logic to send the email.
|
||||
// This is a placeholder function.
|
||||
console.log(`Sending email to ${email} with subject "${subject}" and body "${body}"`);
|
||||
console.log(`QR Code Base64: ${qr_code_base64}`);
|
||||
}
|
||||
</script>
|
||||
|
||||
Pl
|
||||
78
src/routes/private/events/creator/steps/StepOverview.svelte
Normal file
78
src/routes/private/events/creator/steps/StepOverview.svelte
Normal file
@@ -0,0 +1,78 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
let { data, event, participants, email, stepConditions } = $props();
|
||||
|
||||
function redirectToFinish() {
|
||||
// Generate a random variable name
|
||||
const varName = 'event_' + Math.random().toString(36).substr(2, 9);
|
||||
// Save the data to sessionStorage
|
||||
sessionStorage.setItem(
|
||||
varName,
|
||||
JSON.stringify({ event, participants, email })
|
||||
);
|
||||
// Redirect with the variable name as a query parameter
|
||||
goto(`/private/events/creator/finish?data=${encodeURIComponent(varName)}`);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<!-- New Event Overview -->
|
||||
<div class="mb-4 rounded border border-gray-300 bg-white p-4">
|
||||
<h2 class="mb-2 text-xl font-bold">Event Overview</h2>
|
||||
<ul class="space-y-1">
|
||||
<li><span class="font-semibold">Name:</span> {event.name}</li>
|
||||
<li><span class="font-semibold">Date:</span> {event.date}</li>
|
||||
<li><span class="font-semibold">Description:</span> {event.description}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Email Overview -->
|
||||
<div class="mb-4 rounded border border-gray-300 bg-white p-4">
|
||||
<h2 class="mb-2 text-xl font-bold">Email Preview</h2>
|
||||
<div class="mb-2"><span class="font-semibold">Subject:</span> {email.subject}</div>
|
||||
<div class="rounded border bg-gray-50 p-2 whitespace-pre-line text-gray-700">
|
||||
<span class="font-semibold"></span>
|
||||
<div>{email.body}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Participants Overview -->
|
||||
<div class="rounded border border-gray-300 bg-white p-4">
|
||||
<h2 class="mb-2 text-xl font-bold">Participants ({participants.length})</h2>
|
||||
<ul class="space-y-1">
|
||||
{#each participants.slice(0, 10) as p}
|
||||
<li class="flex items-center gap-2 border-b pb-1 last:border-b-0">
|
||||
<span class="font-semibold">{p.name} {p.surname}</span>
|
||||
<span class="flex-1"></span>
|
||||
<span class="text-right font-mono text-xs text-gray-600">{p.email}</span>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
<p class="mt-2 text-sm text-gray-500">Note: Only the first 10 participants are shown.</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onclick={redirectToFinish}
|
||||
class="mt-4 w-full rounded bg-blue-600 px-4 py-3 font-bold text-white
|
||||
transition-colors duration-200 hover:bg-blue-700
|
||||
disabled:cursor-not-allowed disabled:bg-gray-300 disabled:text-gray-500"
|
||||
disabled={!stepConditions.every(Boolean)}
|
||||
>
|
||||
Generate QR codes and send
|
||||
</button>
|
||||
|
||||
<div class="mt-2 space-y-1">
|
||||
{#if !stepConditions[0]}
|
||||
<p class="text-sm text-red-500">Please provide an event name before proceeding.</p>
|
||||
{/if}
|
||||
{#if !stepConditions[1]}
|
||||
<p class="text-sm text-red-500">Please add at least one participant before proceeding.</p>
|
||||
{/if}
|
||||
{#if !stepConditions[2]}
|
||||
<p class="text-sm text-red-500">Please provide an email subject before proceeding.</p>
|
||||
{/if}
|
||||
{#if !stepConditions[3]}
|
||||
<p class="text-sm text-red-500">Please provide an email body before proceeding.</p>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,65 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
|
||||
let { participants = [] } = $props();
|
||||
let loading = $state(false);
|
||||
|
||||
function handleEnhance() {
|
||||
loading = true;
|
||||
|
||||
return async ({ update }) => {
|
||||
await update();
|
||||
loading = false;
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<form
|
||||
method="POST"
|
||||
action="?/participants"
|
||||
use:enhance={handleEnhance}
|
||||
enctype="multipart/form-data"
|
||||
class="flex w-full flex-col space-y-4 rounded border border-gray-300 bg-white p-8 shadow-none"
|
||||
>
|
||||
<h2 class="mb-4 text-center text-2xl font-semibold">Upload Participants</h2>
|
||||
<label class="flex flex-col text-gray-700">
|
||||
CSV File
|
||||
<input
|
||||
type="file"
|
||||
name="participants"
|
||||
id="participants"
|
||||
accept=".csv"
|
||||
class="mt-1 rounded border border-gray-300 px-3 py-2 focus:ring-2 focus:ring-blue-200 focus:outline-none"
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
type="submit"
|
||||
class="w-full rounded bg-blue-600 py-2 text-white transition hover:bg-blue-700"
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{#if participants.length === 0}
|
||||
<div class="mt-4 rounded border-l-4 border-gray-500 bg-gray-100 p-4 text-gray-700">
|
||||
{#if loading}
|
||||
<strong>Loading...</strong>
|
||||
{:else}
|
||||
<strong>No participants yet...</strong>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="mt-4 rounded border-l-4 border-green-500 bg-green-50 p-4 text-green-700">
|
||||
<ul class="space-y-2">
|
||||
{#each participants as p, i}
|
||||
<li class="flex items-center justify-between border-b pb-1">
|
||||
<div>
|
||||
<div class="font-semibold">{p.name} {p.surname}</div>
|
||||
<div class="font-mono text-xs text-gray-600">{p.email}</div>
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
Reference in New Issue
Block a user