AkurAI Build
Menu

Public repository / b4fe8663e5af6be441dafce506787af75d7ec4f1

AkurAI-Mail / src/routes/(admin)/mailboxes/+page.svelte

Latest change dc540296357442733693769e22cd6944f7c2a721 - refactor: split admin UI into separate routes with shared sidebar layout by Ólafur Búi Ólafsson

<script lang="ts">
  let { data } = $props();
  let users = $derived(data.users);
  let aliases = $derived(data.aliases);
  let userEmail = $state("");
  let alias = $state("");
  let target = $state("");
  let result = $state<any>(null);
  let busy = $state(false);

  async function refresh() {
    const response = await fetch(location.href, { headers: { accept: "text/html" } });
    if (response.ok) location.reload();
  }

  async function action(payload: Record<string, any>) {
    busy = true;
    result = null;
    try {
      const response = await fetch("/api/actions", {
        method: "POST",
        headers: { "content-type": "application/json" },
        body: JSON.stringify(payload)
      });
      result = await response.json();
      if (result.ok) await refresh();
    } finally {
      busy = false;
    }
  }
</script>

<svelte:head><title>Mailboxes - AkurAI Mail Admin</title></svelte:head>

<header class="topbar">
  <div>
    <p class="kicker">Mailbox management</p>
    <h1>Mailboxes &amp; Aliases</h1>
    <p>{users.length} mailboxes, {aliases.length} aliases</p>
  </div>
  <button class="primary" onclick={refresh} disabled={busy}>Refresh</button>
</header>

{#if result?.password}
  <section class="notice success" aria-live="polite"><strong>Temporary password</strong><code>{result.password}</code></section>
{:else if result?.ok}
  <section class="notice success" aria-live="polite"><strong>Action complete</strong></section>
{:else if result?.error}
  <section class="notice error" role="alert"><strong>Action failed</strong><span>{result.error}</span></section>
{/if}

<section class="split">
  <article class="panel">
    <div class="panel-head">
      <div>
        <h2>Mailboxes</h2>
        <p>{users.length} active</p>
      </div>
    </div>
    <form onsubmit={(e) => { e.preventDefault(); action({ action: "add-user", email: userEmail }); }}>
      <label>
        <span>Email</span>
        <input bind:value={userEmail} type="email" placeholder="name@olibuijr.com" required />
      </label>
      <button class="primary" disabled={busy}>Add</button>
    </form>
    <div class="table">
      {#each users as user}
        <div>
          <span><strong>{user.email}</strong><small>{user.quotaUsedMb ?? 0} MB used · last delivery {user.lastDelivery ? new Date(user.lastDelivery * 1000).toLocaleString() : "none"}</small></span>
          <a class="mini-link" href={`/webmail?mailbox=${encodeURIComponent(user.email)}`}>Open webmail</a>
        </div>
      {:else}
        <p class="empty">No mailboxes are configured.</p>
      {/each}
    </div>
  </article>

  <article class="panel">
    <div class="panel-head">
      <div>
        <h2>Aliases</h2>
        <p>{aliases.length} active</p>
      </div>
    </div>
    <form class="alias-form" onsubmit={(e) => { e.preventDefault(); action({ action: "add-alias", alias, target }); }}>
      <label>
        <span>Alias</span>
        <input bind:value={alias} type="email" placeholder="alias@olibuijr.com" required />
      </label>
      <label>
        <span>Target</span>
        <input bind:value={target} type="email" placeholder="target@olibuijr.com" required />
      </label>
      <button class="primary" disabled={busy}>Add</button>
    </form>
    <div class="table">
      {#each aliases as item}
        <div>
          <span>{item.alias}</span>
          <em>{item.target}</em>
        </div>
      {:else}
        <p class="empty">No aliases are configured.</p>
      {/each}
    </div>
  </article>
</section>

<style>
  .topbar{display:flex;justify-content:space-between;align-items:flex-start;gap:18px;min-width:0}
  .kicker{margin:0 0 6px;color:#126b5a;font-size:13px;font-weight:800}.topbar h1{margin:0;font-size:30px;line-height:1.15;text-wrap:balance}.topbar p:last-child{margin:6px 0 0;color:#61716c;overflow-wrap:anywhere}
  .primary{border:0;border-radius:6px;background:#126b5a;color:#fff;min-height:44px;padding:10px 15px;font-weight:800;cursor:pointer}.primary:hover{background:#0e594b}.primary:disabled{opacity:.55;cursor:not-allowed}
  .panel,.notice{background:#fff;border:1px solid #dce4e1;border-radius:8px;min-width:0}
  .panel{padding:18px}.panel-head{display:flex;justify-content:space-between;align-items:flex-start;gap:12px;margin-bottom:15px}.panel h2{margin:0;font-size:18px}.panel p{margin:4px 0 0;color:#61716c}
  .split{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:18px}
  form{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:10px;margin-bottom:14px}.alias-form{grid-template-columns:minmax(0,1fr) minmax(0,1fr) auto}label{display:grid;gap:6px;min-width:0}label span{font-size:13px;font-weight:800;color:#33413d}input{width:100%;min-width:0;border:1px solid #cbd8d4;border-radius:6px;padding:11px;background:#fff;color:#151d1b}input:focus{outline:3px solid #b7e7da;border-color:#126b5a}
  .table{display:grid;gap:8px}.table div{display:grid;align-items:center;gap:10px;border-top:1px solid #edf1ef;padding-top:10px;min-width:0;grid-template-columns:minmax(0,1fr) auto}
  .table span,.table em{min-width:0;overflow-wrap:anywhere}.table span small{display:block;color:#61716c;margin-top:3px}.table em,.mini-link{font-style:normal;color:#126b5a;background:#e9f6f2;border-radius:999px;padding:4px 8px;font-size:12px;font-weight:800;text-decoration:none}
  .notice{display:flex;align-items:center;gap:12px;padding:14px 16px}.notice.success{border-color:#a9d9c9}.notice.error{border-color:#d49a91}.notice code{background:#eef4f1;border-radius:5px;padding:5px 7px;white-space:pre-wrap;word-break:break-word}.empty{color:#61716c}
  @media(max-width:1050px){.split{grid-template-columns:minmax(0,1fr)}}
  @media(max-width:760px){.topbar{display:grid}.topbar h1{font-size:25px}.primary{width:100%}form,.alias-form{grid-template-columns:minmax(0,1fr)}.table div{grid-template-columns:minmax(0,1fr)}}
  @media(max-width:420px){.panel{padding:15px}.topbar h1{font-size:23px}}
</style>