190 lines
3.9 KiB
Svelte
190 lines
3.9 KiB
Svelte
<script lang="ts">
|
|
interface Domain {
|
|
id: number;
|
|
name: string;
|
|
userCount: number;
|
|
aliasCount: number;
|
|
}
|
|
|
|
let domains = $state<Domain[]>([]);
|
|
let loading = $state(true);
|
|
let showModal = $state(false);
|
|
let newDomain = $state({ name: '' });
|
|
|
|
async function loadDomains() {
|
|
try {
|
|
const res = await fetch('/api/domains');
|
|
if (res.ok) {
|
|
const data = await res.json();
|
|
domains = data.data || [];
|
|
}
|
|
} catch (e) {
|
|
console.error('Failed to load domains:', e);
|
|
} finally {
|
|
loading = false;
|
|
}
|
|
}
|
|
|
|
async function createDomain() {
|
|
try {
|
|
const res = await fetch('/api/domains', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(newDomain)
|
|
});
|
|
if (res.ok) {
|
|
showModal = false;
|
|
newDomain = { name: '' };
|
|
await loadDomains();
|
|
}
|
|
} catch (e) {
|
|
console.error('Failed to create domain:', e);
|
|
}
|
|
}
|
|
|
|
async function deleteDomain(id: number) {
|
|
if (!confirm('Delete this domain? Users and aliases will be deleted.')) return;
|
|
try {
|
|
await fetch(`/api/domains/${id}`, { method: 'DELETE' });
|
|
await loadDomains();
|
|
} catch (e) {
|
|
console.error('Failed to delete domain:', e);
|
|
}
|
|
}
|
|
|
|
$effect(() => { loadDomains(); });
|
|
</script>
|
|
|
|
<div class="header">
|
|
<h2>Domains</h2>
|
|
<button onclick={() => showModal = true}>Add Domain</button>
|
|
</div>
|
|
|
|
{#if loading}
|
|
<p>Loading...</p>
|
|
{:else if domains.length === 0}
|
|
<p>No domains configured yet.</p>
|
|
{:else}
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Domain</th>
|
|
<th>Users</th>
|
|
<th>Aliases</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{#each domains as domain}
|
|
<tr>
|
|
<td><a href="/domains/{domain.name}/users">{domain.name}</a></td>
|
|
<td>{domain.userCount}</td>
|
|
<td>{domain.aliasCount}</td>
|
|
<td>
|
|
<button class="danger" onclick={() => deleteDomain(domain.id)}>Delete</button>
|
|
</td>
|
|
</tr>
|
|
{/each}
|
|
</tbody>
|
|
</table>
|
|
{/if}
|
|
|
|
{#if showModal}
|
|
<div class="modal-backdrop" onclick={() => showModal = false}>
|
|
<div class="modal" onclick={(e) => e.stopPropagation()}>
|
|
<h3>Add Domain</h3>
|
|
<form onsubmit={(e) => { e.preventDefault(); createDomain(); }}>
|
|
<label>
|
|
Domain name:
|
|
<input type="text" bind:value={newDomain.name} placeholder="example.org" required />
|
|
</label>
|
|
<div class="actions">
|
|
<button type="button" onclick={() => showModal = false}>Cancel</button>
|
|
<button type="submit">Create</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<style>
|
|
.header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
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.danger {
|
|
background: #e74c3c;
|
|
}
|
|
|
|
.modal-backdrop {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: rgba(0,0,0,0.5);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 100;
|
|
}
|
|
|
|
.modal {
|
|
background: white;
|
|
padding: 2rem;
|
|
border-radius: 8px;
|
|
min-width: 400px;
|
|
}
|
|
|
|
.modal form {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.modal label {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.modal input {
|
|
padding: 0.5rem;
|
|
border: 1px solid #ddd;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.actions {
|
|
display: flex;
|
|
gap: 1rem;
|
|
justify-content: flex-end;
|
|
}
|
|
</style>
|