Complete daisyUI migration with CSS fixes

- Migrate all pages to daisyUI components
- Add data-theme="light" to app.html for proper theme
- Fix config to load .env from multiple locations
- Update avatar to use avatar-placeholder pattern
- Use @tailwindcss/vite plugin instead of PostCSS
This commit is contained in:
Christoph Haas 2026-03-23 21:09:32 +01:00
parent 70aa813c7f
commit 0217f0b4c4
14 changed files with 739 additions and 1564 deletions

View file

@ -37,81 +37,46 @@
});
</script>
{#if loading}
<p>Loading...</p>
{:else if error}
<p class="error">{error}</p>
{:else if domain}
<div class="header">
<a href="/domains" class="back">← Back to Domains</a>
<h2>{domain.name}</h2>
<div class="space-y-6">
<div class="flex items-center gap-4">
<a href="/domains" class="btn btn-ghost btn-sm">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M9.707 16.707a1 1 0 01-1.414 0l-6-6a1 1 0 010-1.414l6-6a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l4.293 4.293a1 1 0 010 1.414z" clip-rule="evenodd" />
</svg>
Back to Domains
</a>
{#if domain}
<h2 class="text-2xl font-bold">{domain.name}</h2>
{/if}
</div>
<div class="cards-grid">
<a href="/domains/{domain.name}/users" class="card">
<h3>{domain.userCount}</h3>
<p>Users</p>
</a>
<a href="/domains/{domain.name}/aliases" class="card">
<h3>{domain.aliasCount}</h3>
<p>Aliases</p>
</a>
</div>
{:else}
<p>Domain not found</p>
{/if}
<style>
.header {
margin-bottom: 2rem;
}
.back {
display: inline-block;
margin-bottom: 1rem;
color: #3498db;
text-decoration: none;
}
.back:hover {
text-decoration: underline;
}
.error {
color: #e74c3c;
}
.cards-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1.5rem;
}
.card {
background: white;
padding: 2rem;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
text-align: center;
text-decoration: none;
color: inherit;
transition: transform 0.2s, box-shadow 0.2s;
}
.card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
.card h3 {
font-size: 3rem;
margin: 0;
color: #3498db;
}
.card p {
margin: 0.5rem 0 0;
color: #7f8c8d;
font-size: 1.1rem;
}
</style>
{#if loading}
<div class="flex justify-center py-12">
<span class="loading loading-spinner loading-lg"></span>
</div>
{:else if error}
<div class="alert alert-error">
<span>{error}</span>
</div>
{:else if domain}
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 max-w-2xl">
<a href="/domains/{domain.name}/users" class="card bg-base-100 shadow hover:shadow-md transition-shadow">
<div class="card-body items-center text-center">
<h2 class="card-title text-4xl font-bold text-primary">{domain.userCount}</h2>
<p class="text-base-content/60">Users</p>
</div>
</a>
<a href="/domains/{domain.name}/aliases" class="card bg-base-100 shadow hover:shadow-md transition-shadow">
<div class="card-body items-center text-center">
<h2 class="card-title text-4xl font-bold text-secondary">{domain.aliasCount}</h2>
<p class="text-base-content/60">Aliases</p>
</div>
</a>
</div>
{:else}
<div class="alert">
<span>Domain not found</span>
</div>
{/if}
</div>

View file

@ -92,192 +92,102 @@
});
</script>
<div class="header">
<a href="/domains/{domainName}" class="back">← Back to {domainName}</a>
<h2>Aliases</h2>
<button onclick={openModal}>Add Alias</button>
<div class="space-y-4">
<div class="flex justify-between items-center">
<div class="flex items-center gap-4">
<a href="/domains/{domainName}" class="btn btn-ghost btn-sm">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd" />
</svg>
Back
</a>
<h2 class="text-2xl font-bold">Aliases - {domainName}</h2>
</div>
<button class="btn btn-primary" onclick={openModal}>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd" />
</svg>
Add Alias
</button>
</div>
{#if loading}
<div class="flex justify-center py-12">
<span class="loading loading-spinner loading-lg"></span>
</div>
{:else if aliases.length === 0}
<div class="card bg-base-100 shadow">
<div class="card-body items-center text-center py-12">
<p class="text-base-content/60">No aliases configured yet.</p>
</div>
</div>
{:else}
<div class="card bg-base-100 shadow overflow-hidden">
<table class="table">
<thead>
<tr>
<th>Source</th>
<th>Destination</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{#each aliases as alias}
<tr class="hover">
<td class="font-medium">{alias.source}</td>
<td>{alias.destination}</td>
<td>
<button class="btn btn-error btn-sm" onclick={() => deleteAlias(alias.id)}>
Delete
</button>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
</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>
<dialog bind:this={dialogEl} class="modal">
<div class="modal-box">
<h3 class="font-bold text-lg mb-4">Add Alias</h3>
<form onsubmit={(e) => { e.preventDefault(); createAlias(); }}>
<fieldset class="fieldset">
<legend class="fieldset-legend">Source (alias address)</legend>
<div class="join w-full">
<input
type="text"
class="input input-bordered join-item flex-1"
bind:value={sourceLocalPart}
placeholder="alias-name"
required
/>
<span class="btn btn-disabled join-item">@{domainName}</span>
</div>
</fieldset>
<fieldset class="fieldset mt-4">
<legend class="fieldset-legend">Destination (forward to)</legend>
<input
type="text"
class="input input-bordered w-full"
bind:value={newAlias.destination}
placeholder="user@example.org"
required
/>
</fieldset>
{#if createError}
<div class="alert alert-error mt-4">
<span>{createError}</span>
</div>
{/if}
<div class="modal-action">
<button type="button" class="btn" onclick={closeModal}>Cancel</button>
<button type="submit" class="btn btn-primary">Create</button>
</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>
</div>
<form method="dialog" class="modal-backdrop">
<button onclick={closeModal}>close</button>
</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>

View file

@ -114,198 +114,112 @@
});
</script>
<div class="header">
<a href="/domains/{domainName}" class="back">← Back to {domainName}</a>
<h2>Users</h2>
<button onclick={openModal}>Add User</button>
<div class="space-y-4">
<div class="flex justify-between items-center">
<div class="flex items-center gap-4">
<a href="/domains/{domainName}" class="btn btn-ghost btn-sm">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd" />
</svg>
Back
</a>
<h2 class="text-2xl font-bold">Users - {domainName}</h2>
</div>
<button class="btn btn-primary" onclick={openModal}>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd" />
</svg>
Add User
</button>
</div>
{#if loading}
<div class="flex justify-center py-12">
<span class="loading loading-spinner loading-lg"></span>
</div>
{:else if users.length === 0}
<div class="card bg-base-100 shadow">
<div class="card-body items-center text-center py-12">
<p class="text-base-content/60">No users configured yet.</p>
</div>
</div>
{:else}
<div class="card bg-base-100 shadow overflow-hidden">
<table class="table">
<thead>
<tr>
<th>Email</th>
<th>Quota</th>
<th>Used</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{#each users as user}
<tr class="hover">
<td class="font-medium">{user.email}</td>
<td>{formatQuota(user.quota)}</td>
<td>{formatUsed(user.usedQuota, user.quota)}</td>
<td>
<button class="btn btn-error btn-sm" onclick={() => deleteUser(user.id)}>
Delete
</button>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
</div>
{#if loading}
<p>Loading...</p>
{:else if users.length === 0}
<p>No users configured yet.</p>
{:else}
<table>
<thead>
<tr>
<th>Email</th>
<th>Quota</th>
<th>Used</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{#each users as user}
<tr>
<td>{user.email}</td>
<td>{formatQuota(user.quota)}</td>
<td>{formatUsed(user.usedQuota, user.quota)}</td>
<td>
<button class="danger" onclick={() => deleteUser(user.id)}>Delete</button>
</td>
</tr>
{/each}
</tbody>
</table>
{/if}
<dialog bind:this={dialogEl}>
<h3>Add User</h3>
<form onsubmit={(e) => { e.preventDefault(); createUser(); }}>
<label class="email-input">
<span class="label-text">Email:</span>
<div class="input-group">
<input type="text" bind:value={localPart} placeholder="username" required />
<span class="domain-suffix">@{domainName}</span>
<dialog bind:this={dialogEl} class="modal">
<div class="modal-box">
<h3 class="font-bold text-lg mb-4">Add User</h3>
<form onsubmit={(e) => { e.preventDefault(); createUser(); }}>
<fieldset class="fieldset">
<legend class="fieldset-legend">Username</legend>
<div class="join w-full">
<input
type="text"
class="input input-bordered join-item flex-1"
bind:value={localPart}
placeholder="username"
required
/>
<span class="btn btn-disabled join-item">@{domainName}</span>
</div>
</fieldset>
<fieldset class="fieldset mt-4">
<legend class="fieldset-legend">Password</legend>
<input
type="password"
class="input input-bordered w-full"
bind:value={newUser.password}
required
/>
</fieldset>
<fieldset class="fieldset mt-4">
<legend class="fieldset-legend">Quota (bytes, 0 = default)</legend>
<input
type="number"
class="input input-bordered w-full"
bind:value={newUser.quota}
min="0"
/>
</fieldset>
{#if createError}
<div class="alert alert-error mt-4">
<span>{createError}</span>
</div>
{/if}
<div class="modal-action">
<button type="button" class="btn" onclick={closeModal}>Cancel</button>
<button type="submit" class="btn btn-primary">Create</button>
</div>
</label>
<label>
Password:
<input type="password" bind:value={newUser.password} required />
</label>
<label>
Quota (bytes, 0 = default):
<input type="number" bind:value={newUser.quota} min="0" />
</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>
</div>
<form method="dialog" class="modal-backdrop">
<button onclick={closeModal}>close</button>
</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>