imc-vibe/frontend/src/routes/domains/[name]/aliases/+page.svelte
Christoph Haas c692fe436f Improve user/alias creation forms
- Prefill domain part in forms (e.g. input + @domain.com)
- Use native <dialog> element for modals
- Show error messages instead of alert()
- Cleaner input group styling
2026-03-23 00:00:42 +01:00

283 lines
5.9 KiB
Svelte

<script lang="ts">
import { page } from '$app/stores';
interface Alias {
id: number;
source: string;
destination: string;
}
let aliases = $state<Alias[]>([]);
let loading = $state(true);
let dialogEl = $state<HTMLDialogElement | null>(null);
let sourceLocalPart = $state('');
let newAlias = $state({ destination: '' });
let createError = $state('');
let domainName = $state('');
async function loadAliases(name: string) {
try {
const token = localStorage.getItem('token');
const res = await fetch(`/api/domains/${encodeURIComponent(name)}/aliases`, {
headers: { Authorization: `Bearer ${token}` }
});
if (res.ok) {
const data = await res.json();
aliases = data.data || [];
}
} catch (e) {
console.error('Failed to load aliases:', e);
} finally {
loading = false;
}
}
function openModal() {
createError = '';
sourceLocalPart = '';
newAlias = { destination: '' };
dialogEl?.showModal();
}
function closeModal() {
dialogEl?.close();
}
async function createAlias() {
createError = '';
const source = `${sourceLocalPart}@${domainName}`;
try {
const token = localStorage.getItem('token');
const res = await fetch(`/api/domains/${encodeURIComponent(domainName)}/aliases`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
},
body: JSON.stringify({ source, destination: newAlias.destination })
});
if (res.ok) {
closeModal();
await loadAliases(domainName);
} else {
const error = await res.json();
createError = error.error || 'Failed to create alias';
}
} catch (e) {
createError = 'Failed to create alias';
console.error('Failed to create alias:', e);
}
}
async function deleteAlias(id: number) {
if (!confirm('Delete this alias?')) return;
try {
const token = localStorage.getItem('token');
await fetch(`/api/domains/${encodeURIComponent(domainName)}/aliases/${id}`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${token}` }
});
await loadAliases(domainName);
} catch (e) {
console.error('Failed to delete alias:', e);
}
}
$effect(() => {
const name = $page.params.name;
if (name) {
domainName = name;
loadAliases(name);
}
});
</script>
<div class="header">
<a href="/domains/{domainName}" class="back">← Back to {domainName}</a>
<h2>Aliases</h2>
<button onclick={openModal}>Add Alias</button>
</div>
{#if loading}
<p>Loading...</p>
{:else if aliases.length === 0}
<p>No aliases configured yet.</p>
{:else}
<table>
<thead>
<tr>
<th>Source</th>
<th>Destination</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{#each aliases as alias}
<tr>
<td>{alias.source}</td>
<td>{alias.destination}</td>
<td>
<button class="danger" onclick={() => deleteAlias(alias.id)}>Delete</button>
</td>
</tr>
{/each}
</tbody>
</table>
{/if}
<dialog bind:this={dialogEl}>
<h3>Add Alias</h3>
<form onsubmit={(e) => { e.preventDefault(); createAlias(); }}>
<label class="email-input">
<span class="label-text">Source (alias address):</span>
<div class="input-group">
<input type="text" bind:value={sourceLocalPart} placeholder="alias-name" required />
<span class="domain-suffix">@{domainName}</span>
</div>
</label>
<label>
Destination (forward to):
<input type="text" bind:value={newAlias.destination} placeholder="user@example.org" required />
</label>
{#if createError}
<p class="error">{createError}</p>
{/if}
<div class="actions">
<button type="button" class="secondary" onclick={closeModal}>Cancel</button>
<button type="submit">Create</button>
</div>
</form>
</dialog>
<style>
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
gap: 1rem;
}
.back {
color: #3498db;
text-decoration: none;
font-size: 0.9rem;
}
.back:hover {
text-decoration: underline;
}
.header h2 {
flex: 1;
margin: 0;
}
table {
width: 100%;
background: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
th, td {
padding: 1rem;
text-align: left;
border-bottom: 1px solid #eee;
}
th {
background: #f8f9fa;
font-weight: 600;
}
button {
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
cursor: pointer;
background: #3498db;
color: white;
}
button.secondary {
background: #95a5a6;
}
button.danger {
background: #e74c3c;
}
dialog {
padding: 2rem;
border-radius: 8px;
border: none;
box-shadow: 0 4px 20px rgba(0,0,0,0.2);
}
dialog::backdrop {
background: rgba(0,0,0,0.5);
}
dialog h3 {
margin: 0 0 1rem 0;
}
dialog form {
display: flex;
flex-direction: column;
gap: 1rem;
}
dialog label {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
dialog input {
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
}
.email-input .label-text {
font-weight: normal;
}
.input-group {
display: flex;
align-items: center;
}
.input-group input {
flex: 1;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.domain-suffix {
padding: 0.5rem 0.75rem;
background: #f0f0f0;
border: 1px solid #ddd;
border-left: none;
border-radius: 0 4px 4px 0;
color: #666;
white-space: nowrap;
}
dialog .error {
color: #e74c3c;
background: #fdecea;
padding: 0.5rem;
border-radius: 4px;
margin: 0;
}
.actions {
display: flex;
gap: 1rem;
justify-content: flex-end;
}
</style>