c.id === updated.id ? updated : c,
);
- toasts.success("Contact updated");
+ toasts.success(m.toast_success_contact_updated());
} else {
const created = await createOrgContact(supabase, data.org.id, {
name: formName.trim(),
@@ -123,11 +123,11 @@
notes: formNotes.trim() || undefined,
});
contacts = [...contacts, created];
- toasts.success("Contact added");
+ toasts.success(m.toast_success_contact_added());
}
showModal = false;
} catch {
- toasts.error("Failed to save contact");
+ toasts.error(m.toast_error_save_contact());
} finally {
saving = false;
}
@@ -137,9 +137,9 @@
try {
await deleteOrgContact(supabase, contact.id);
contacts = contacts.filter((c) => c.id !== contact.id);
- toasts.success("Contact removed");
+ toasts.success(m.toast_success_contact_removed());
} catch {
- toasts.error("Failed to delete contact");
+ toasts.error(m.toast_error_delete_contact());
}
}
@@ -190,10 +190,10 @@
bind:value={filterCategory}
placeholder=""
options={[
- { value: "all", label: "All Categories" },
+ { value: "all", label: m.contacts_all_categories() },
...CONTACT_CATEGORIES.map((cat) => ({
value: cat,
- label: cat.charAt(0).toUpperCase() + cat.slice(1),
+ label: CATEGORY_LABELS[cat] ?? cat,
})),
]}
/>
@@ -399,7 +399,7 @@
placeholder=""
options={CONTACT_CATEGORIES.map((cat) => ({
value: cat,
- label: cat.charAt(0).toUpperCase() + cat.slice(1),
+ label: CATEGORY_LABELS[cat] ?? cat,
}))}
/>
@@ -441,14 +441,18 @@
(showModal = false)}>{m.btn_cancel()}
diff --git a/src/routes/[orgSlug]/events/[eventSlug]/dept/[deptId]/+page.svelte b/src/routes/[orgSlug]/events/[eventSlug]/dept/[deptId]/+page.svelte
index e2a0f01..50ab3d3 100644
--- a/src/routes/[orgSlug]/events/[eventSlug]/dept/[deptId]/+page.svelte
+++ b/src/routes/[orgSlug]/events/[eventSlug]/dept/[deptId]/+page.svelte
@@ -315,7 +315,7 @@
await updateDashboardLayout(supabase, dashboard.id, layout);
dashboard = { ...dashboard, layout };
} catch {
- toasts.error("Failed to update layout");
+ toasts.error(m.toast_error_update_layout());
}
}
@@ -361,7 +361,7 @@
showAddModuleModal = false;
} catch {
- toasts.error("Failed to add module");
+ toasts.error(m.toast_error_add_module());
}
}
@@ -375,7 +375,7 @@
panels: dashboard.panels.filter((p) => p.id !== panelId),
};
} catch {
- toasts.error("Failed to remove module");
+ toasts.error(m.toast_error_remove_module());
}
}
@@ -411,7 +411,7 @@
} catch {
// Revert on failure
dashboard = { ...dashboard, panels: sorted };
- toasts.error("Failed to reorder modules");
+ toasts.error(m.toast_error_reorder_modules());
}
}
@@ -436,7 +436,7 @@
c.id === checklistId ? { ...c, items: [...c.items, item] } : c,
);
} catch {
- toasts.error("Failed to add item");
+ toasts.error(m.toast_error_add_item());
}
}
@@ -458,7 +458,7 @@
i.id === itemId ? { ...i, is_completed: !checked } : i,
),
}));
- toasts.error("Failed to update item");
+ toasts.error(m.toast_error_update_item());
}
}
@@ -472,7 +472,7 @@
await deleteChecklistItem(supabase, itemId);
} catch {
checklists = prev;
- toasts.error("Failed to delete item");
+ toasts.error(m.toast_error_delete_item());
}
}
@@ -486,7 +486,7 @@
),
}));
} catch {
- toasts.error("Failed to update item");
+ toasts.error(m.toast_error_update_item());
}
}
@@ -495,7 +495,7 @@
const cl = await createChecklist(supabase, department.id, title);
checklists = [...checklists, { ...cl, items: [] }];
} catch {
- toasts.error("Failed to create checklist");
+ toasts.error(m.toast_error_create_checklist());
}
}
@@ -504,7 +504,7 @@
await deleteChecklist(supabase, checklistId);
checklists = checklists.filter((c) => c.id !== checklistId);
} catch {
- toasts.error("Failed to delete checklist");
+ toasts.error(m.toast_error_delete_checklist());
}
}
@@ -515,7 +515,7 @@
c.id === checklistId ? { ...c, title } : c,
);
} catch {
- toasts.error("Failed to rename checklist");
+ toasts.error(m.toast_error_rename_checklist());
}
}
@@ -528,7 +528,7 @@
const note = await createNote(supabase, department.id, title);
notes = [...notes, note];
} catch {
- toasts.error("Failed to create note");
+ toasts.error(m.toast_error_create_note());
}
}
@@ -540,7 +540,7 @@
const updated = await updateNote(supabase, noteId, params);
notes = notes.map((n) => (n.id === noteId ? updated : n));
} catch {
- toasts.error("Failed to update note");
+ toasts.error(m.toast_error_update_note());
}
}
@@ -549,7 +549,7 @@
await deleteNote(supabase, noteId);
notes = notes.filter((n) => n.id !== noteId);
} catch {
- toasts.error("Failed to delete note");
+ toasts.error(m.toast_error_delete_note());
}
}
@@ -567,7 +567,7 @@
);
scheduleStages = [...scheduleStages, stage];
} catch {
- toasts.error("Failed to create stage");
+ toasts.error(m.toast_error_create_stage());
}
}
@@ -576,7 +576,7 @@
await deleteStageApi(supabase, stageId);
scheduleStages = scheduleStages.filter((s) => s.id !== stageId);
} catch {
- toasts.error("Failed to delete stage");
+ toasts.error(m.toast_error_delete_stage());
}
}
@@ -597,7 +597,7 @@
new Date(b.start_time).getTime(),
);
} catch {
- toasts.error("Failed to create schedule block");
+ toasts.error(m.toast_error_create_block());
}
}
@@ -626,7 +626,7 @@
new Date(b.start_time).getTime(),
);
} catch {
- toasts.error("Failed to update schedule block");
+ toasts.error(m.toast_error_update_block());
}
}
@@ -635,7 +635,7 @@
await deleteBlockApi(supabase, blockId);
scheduleBlocks = scheduleBlocks.filter((b) => b.id !== blockId);
} catch {
- toasts.error("Failed to delete schedule block");
+ toasts.error(m.toast_error_delete_block());
}
}
@@ -664,7 +664,7 @@
a.name.localeCompare(b.name),
);
} catch {
- toasts.error("Failed to create contact");
+ toasts.error(m.toast_error_create_contact());
}
}
@@ -691,7 +691,7 @@
.map((c) => (c.id === contactId ? updated : c))
.sort((a, b) => a.name.localeCompare(b.name));
} catch {
- toasts.error("Failed to update contact");
+ toasts.error(m.toast_error_update_contact());
}
}
@@ -700,7 +700,7 @@
await deleteContactApi(supabase, contactId);
contacts = contacts.filter((c) => c.id !== contactId);
} catch {
- toasts.error("Failed to delete contact");
+ toasts.error(m.toast_error_delete_contact());
}
}
@@ -718,7 +718,7 @@
);
budgetCategories = [...budgetCategories, cat];
} catch {
- toasts.error("Failed to create category");
+ toasts.error(m.toast_error_create_category());
}
}
@@ -729,7 +729,7 @@
(c) => c.id !== categoryId,
);
} catch {
- toasts.error("Failed to delete category");
+ toasts.error(m.toast_error_delete_category());
}
}
@@ -749,7 +749,7 @@
);
budgetItems = [...budgetItems, item];
} catch {
- toasts.error("Failed to create budget item");
+ toasts.error(m.toast_error_create_budget_item());
}
}
@@ -773,7 +773,7 @@
i.id === itemId ? updated : i,
);
} catch {
- toasts.error("Failed to update budget item");
+ toasts.error(m.toast_error_update_budget_item());
}
}
@@ -782,7 +782,7 @@
await deleteBudgetItemApi(supabase, itemId);
budgetItems = budgetItems.filter((i) => i.id !== itemId);
} catch {
- toasts.error("Failed to delete budget item");
+ toasts.error(m.toast_error_delete_budget_item());
}
}
@@ -845,9 +845,11 @@
? { ...i, receipt_document_id: doc.id }
: i,
);
- toasts.success(`Receipt "${file.name}" attached`);
+ toasts.success(
+ m.toast_success_receipt_attached({ name: file.name }),
+ );
} catch {
- toasts.error("Failed to upload receipt");
+ toasts.error(m.toast_error_upload_receipt());
} finally {
receiptTargetItemId = null;
}
@@ -872,7 +874,7 @@
);
sponsorTiers = [...sponsorTiers, tier];
} catch {
- toasts.error("Failed to create tier");
+ toasts.error(m.toast_error_create_tier());
}
}
@@ -881,7 +883,7 @@
await deleteSponsorTierApi(supabase, tierId);
sponsorTiers = sponsorTiers.filter((t) => t.id !== tierId);
} catch {
- toasts.error("Failed to delete tier");
+ toasts.error(m.toast_error_delete_tier());
}
}
@@ -906,7 +908,7 @@
a.name.localeCompare(b.name),
);
} catch {
- toasts.error("Failed to create sponsor");
+ toasts.error(m.toast_error_create_sponsor());
}
}
@@ -933,7 +935,7 @@
.map((s) => (s.id === sponsorId ? updated : s))
.sort((a, b) => a.name.localeCompare(b.name));
} catch {
- toasts.error("Failed to update sponsor");
+ toasts.error(m.toast_error_update_sponsor());
}
}
@@ -945,7 +947,7 @@
(d) => d.sponsor_id !== sponsorId,
);
} catch {
- toasts.error("Failed to delete sponsor");
+ toasts.error(m.toast_error_delete_sponsor());
}
}
@@ -963,7 +965,7 @@
);
sponsorDeliverables = [...sponsorDeliverables, del];
} catch {
- toasts.error("Failed to create deliverable");
+ toasts.error(m.toast_error_create_deliverable());
}
}
@@ -983,7 +985,7 @@
sponsorDeliverables = sponsorDeliverables.map((d) =>
d.id === deliverableId ? { ...d, is_completed: !completed } : d,
);
- toasts.error("Failed to update deliverable");
+ toasts.error(m.toast_error_update_deliverable());
}
}
@@ -994,7 +996,7 @@
(d) => d.id !== deliverableId,
);
} catch {
- toasts.error("Failed to delete deliverable");
+ toasts.error(m.toast_error_delete_deliverable());
}
}
diff --git a/src/routes/[orgSlug]/events/[eventSlug]/finances/+page.svelte b/src/routes/[orgSlug]/events/[eventSlug]/finances/+page.svelte
index eb2df3d..39cb9e7 100644
--- a/src/routes/[orgSlug]/events/[eventSlug]/finances/+page.svelte
+++ b/src/routes/[orgSlug]/events/[eventSlug]/finances/+page.svelte
@@ -182,7 +182,7 @@
const dept = data.departments.find((d) => d.id === deptId);
if (dept) (dept as any).planned_budget = val;
} catch {
- toasts.error("Failed to update planned budget");
+ toasts.error(m.toast_error_update_planned_budget());
}
editingDeptId = null;
}
@@ -240,7 +240,7 @@
}
showAllocModal = false;
} catch {
- toasts.error("Failed to save allocation");
+ toasts.error(m.toast_error_save_allocation());
}
}
@@ -251,7 +251,7 @@
(a) => a.id !== allocId,
);
} catch {
- toasts.error("Failed to delete allocation");
+ toasts.error(m.toast_error_delete_allocation());
}
}
diff --git a/src/routes/[orgSlug]/events/[eventSlug]/sponsors/+page.svelte b/src/routes/[orgSlug]/events/[eventSlug]/sponsors/+page.svelte
index 79e7319..23a2d10 100644
--- a/src/routes/[orgSlug]/events/[eventSlug]/sponsors/+page.svelte
+++ b/src/routes/[orgSlug]/events/[eventSlug]/sponsors/+page.svelte
@@ -154,7 +154,7 @@
sponsors = sponsors.map((s) =>
s.id === updated.id ? updated : s,
);
- toasts.success("Sponsor updated");
+ toasts.success(m.toast_success_sponsor_updated());
} else {
const created = await createSponsor(supabase, formDeptId, {
name: formName.trim(),
@@ -168,11 +168,11 @@
notes: formNotes.trim() || undefined,
});
sponsors = [...sponsors, created];
- toasts.success("Sponsor added");
+ toasts.success(m.toast_success_sponsor_added());
}
showModal = false;
} catch {
- toasts.error("Failed to save sponsor");
+ toasts.error(m.toast_error_save_sponsor());
} finally {
saving = false;
}
@@ -182,9 +182,9 @@
try {
await deleteSponsor(supabase, sponsor.id);
sponsors = sponsors.filter((s) => s.id !== sponsor.id);
- toasts.success("Sponsor removed");
+ toasts.success(m.toast_success_sponsor_removed());
} catch {
- toasts.error("Failed to delete sponsor");
+ toasts.error(m.toast_error_delete_sponsor());
}
}
@@ -271,7 +271,7 @@
bind:value={filterStatus}
placeholder=""
options={[
- { value: "all", label: "All Statuses" },
+ { value: "all", label: m.sponsors_all_statuses() },
...Object.entries(STATUS_LABELS).map(([val, label]) => ({
value: val,
label,
@@ -284,7 +284,7 @@
bind:value={filterTier}
placeholder=""
options={[
- { value: "all", label: "All Tiers" },
+ { value: "all", label: m.sponsors_all_tiers() },
...tiers.map((t) => ({ value: t.id, label: t.name })),
]}
/>
@@ -507,14 +507,18 @@
(showModal = false)}>{m.btn_cancel()}
diff --git a/src/routes/[orgSlug]/events/[eventSlug]/tasks/+page.svelte b/src/routes/[orgSlug]/events/[eventSlug]/tasks/+page.svelte
index 03c28fe..b9a002f 100644
--- a/src/routes/[orgSlug]/events/[eventSlug]/tasks/+page.svelte
+++ b/src/routes/[orgSlug]/events/[eventSlug]/tasks/+page.svelte
@@ -284,7 +284,7 @@
await reloadColumns();
} catch (e) {
log.error("Failed to create column", { error: e });
- toasts.error("Failed to create column");
+ toasts.error(m.tasks_toast_create_column_failed());
}
}
@@ -294,7 +294,7 @@
taskColumns = taskColumns.filter((c) => c.id !== columnId);
} catch (e) {
log.error("Failed to delete column", { error: e });
- toasts.error("Failed to delete column");
+ toasts.error(m.tasks_toast_delete_column_failed());
}
}
@@ -306,7 +306,7 @@
);
} catch (e) {
log.error("Failed to rename column", { error: e });
- toasts.error("Failed to rename column");
+ toasts.error(m.tasks_toast_rename_column_failed());
}
}
@@ -333,7 +333,7 @@
await reloadColumns();
} catch (e) {
log.error("Failed to create task", { error: e });
- toasts.error("Failed to create task");
+ toasts.error(m.tasks_toast_create_task_failed());
}
}
@@ -346,7 +346,7 @@
}));
} catch (e) {
log.error("Failed to delete task", { error: e });
- toasts.error("Failed to delete task");
+ toasts.error(m.tasks_toast_delete_task_failed());
}
}
@@ -384,14 +384,14 @@
class="material-symbols-rounded mb-4 block text-[48px] leading-none"
>task_alt
- No task columns yet
+ {m.tasks_no_columns()}
{#if canEdit}
{/if}
@@ -403,7 +403,7 @@
(showAddColumnModal = false)}
>
@@ -433,7 +434,7 @@
(showAddCardModal = false)}
>
diff --git a/src/routes/[orgSlug]/events/[eventSlug]/team/+page.svelte b/src/routes/[orgSlug]/events/[eventSlug]/team/+page.svelte
index af5f359..8f52c55 100644
--- a/src/routes/[orgSlug]/events/[eventSlug]/team/+page.svelte
+++ b/src/routes/[orgSlug]/events/[eventSlug]/team/+page.svelte
@@ -151,66 +151,66 @@
const allModules = [
{
id: "kanban",
- label: "Kanban",
+ label: m.module_kanban_label(),
icon: "view_kanban",
color: "#6366f1",
- desc: "Task boards with columns and cards for tracking work",
+ desc: m.module_kanban_desc(),
},
{
id: "files",
- label: "Files",
+ label: m.module_files_label(),
icon: "folder",
color: "#F59E0B",
- desc: "Shared documents, folders, and file storage",
+ desc: m.module_files_desc(),
},
{
id: "checklist",
- label: "Checklist",
+ label: m.module_checklist_label(),
icon: "checklist",
color: "#10B981",
- desc: "Simple to-do lists with progress tracking",
+ desc: m.module_checklist_desc(),
},
{
id: "notes",
- label: "Notes",
+ label: m.module_notes_label(),
icon: "description",
color: "#8B5CF6",
- desc: "Freeform notes and documentation",
+ desc: m.module_notes_desc(),
},
{
id: "schedule",
- label: "Schedule",
+ label: m.module_schedule_label(),
icon: "calendar_today",
color: "#EC4899",
- desc: "Timeline and timetable for stages and sessions",
+ desc: m.module_schedule_desc(),
},
{
id: "contacts",
- label: "Contacts",
+ label: m.module_contacts_label(),
icon: "contacts",
color: "#00A3E0",
- desc: "Vendor and contact directory with categories",
+ desc: m.module_contacts_desc(),
},
{
id: "budget",
- label: "Budget",
+ label: m.module_budget_label(),
icon: "account_balance",
color: "#10B981",
- desc: "Income and expense tracking with planned vs actual",
+ desc: m.module_budget_desc(),
},
{
id: "sponsors",
- label: "Sponsors",
+ label: m.module_sponsors_label(),
icon: "handshake",
color: "#F59E0B",
- desc: "Sponsor CRM with tiers, deliverables, and pipeline",
+ desc: m.module_sponsors_desc(),
},
{
id: "map",
- label: "Map",
+ label: m.module_map_label(),
icon: "map",
color: "#EF4444",
- desc: "Interactive venue map with pins and areas",
+ desc: m.module_map_desc(),
},
] as const;
diff --git a/src/routes/[orgSlug]/kanban/+page.svelte b/src/routes/[orgSlug]/kanban/+page.svelte
index 1a56e6a..35f4c68 100644
--- a/src/routes/[orgSlug]/kanban/+page.svelte
+++ b/src/routes/[orgSlug]/kanban/+page.svelte
@@ -394,7 +394,7 @@
async function handleDeleteColumn(columnId: string) {
if (!selectedBoard) return;
- if (!confirm("Delete this column and all its cards?")) return;
+ if (!confirm(m.kanban_confirm_delete_column())) return;
const { error } = await supabase
.from("kanban_columns")
diff --git a/src/routes/[orgSlug]/settings/+page.svelte b/src/routes/[orgSlug]/settings/+page.svelte
index 5cb063a..a8427f1 100644
--- a/src/routes/[orgSlug]/settings/+page.svelte
+++ b/src/routes/[orgSlug]/settings/+page.svelte
@@ -314,6 +314,7 @@
import { enhance } from "$app/forms";
import { invalidateAll } from "$app/navigation";
+ import * as m from "$lib/paraglide/messages";
import {
Button,
Badge,
@@ -222,14 +223,18 @@
(activeTab = v)}
diff --git a/src/routes/api/send-invite-email/+server.ts b/src/routes/api/send-invite-email/+server.ts
new file mode 100644
index 0000000..cf68f8b
--- /dev/null
+++ b/src/routes/api/send-invite-email/+server.ts
@@ -0,0 +1,112 @@
+import { json } from '@sveltejs/kit';
+import { env } from '$env/dynamic/private';
+import type { RequestHandler } from './$types';
+import { createLogger } from '$lib/utils/logger';
+
+const log = createLogger('api:send-invite-email');
+
+export const POST: RequestHandler = async ({ request, locals }) => {
+ const session = await locals.safeGetSession();
+ if (!session.user) {
+ return json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ if (!env.RESEND_API_KEY) {
+ log.warn('RESEND_API_KEY not configured, skipping email send');
+ return json({ error: 'Email sending not configured' }, { status: 501 });
+ }
+
+ const { email, orgName, inviteUrl, role } = await request.json();
+
+ if (!email || !orgName || !inviteUrl) {
+ return json({ error: 'email, orgName, and inviteUrl are required' }, { status: 400 });
+ }
+
+ try {
+ const res = await fetch('https://api.resend.com/emails', {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${env.RESEND_API_KEY}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ from: `${orgName} `,
+ to: [email],
+ subject: `${orgName} — You're invited to join`,
+ html: buildInviteEmailHtml(orgName, role, inviteUrl),
+ }),
+ });
+
+ if (!res.ok) {
+ const err = await res.json().catch(() => ({}));
+ log.error('Resend API error', { error: err, data: { email, orgName } });
+ return json({ error: err.message || 'Failed to send email' }, { status: 500 });
+ }
+
+ const data = await res.json();
+ return json({ id: data.id, sent: true });
+ } catch (e) {
+ log.error('Failed to send invite email', { error: e });
+ return json({ error: 'Failed to send email' }, { status: 500 });
+ }
+};
+
+function buildInviteEmailHtml(orgName: string, role: string, inviteUrl: string): string {
+ return `
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+ 👥
+
+ |
+
+
+
+ You're Invited!
+ |
+
+
+ |
+ You've been invited to join
+ |
+
+
+ |
+ ${orgName}
+ |
+
+
+ |
+ as ${role}
+ |
+
+
+ |
+
+ Accept Invitation
+
+ |
+
+
+ |
+ This invite expires in 7 days.
+ |
+
+
+ |
+
+
+
+`;
+}
diff --git a/src/routes/invite/[token]/+page.svelte b/src/routes/invite/[token]/+page.svelte
index b0c5d8f..e381a78 100644
--- a/src/routes/invite/[token]/+page.svelte
+++ b/src/routes/invite/[token]/+page.svelte
@@ -1,9 +1,10 @@