finish early dashboard prototype
This commit is contained in:
parent
14535219d9
commit
7d15d70432
13 changed files with 311 additions and 73 deletions
|
|
@ -60,3 +60,12 @@ hr {
|
|||
border: none;
|
||||
border-bottom: 1px solid var(--bg-2);
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--on-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
|
|
|||
74
src/web/frontend/src/components/ui/AccountAddButton.svelte
Normal file
74
src/web/frontend/src/components/ui/AccountAddButton.svelte
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
<script lang="ts">
|
||||
import { Plus } from '@lucide/svelte';
|
||||
|
||||
let { onclick, onkeydown } = $props();
|
||||
</script>
|
||||
|
||||
<div class="add-account" onclick={onclick} onkeydown={onkeydown} role="button" tabindex="0">
|
||||
<div class="icon-container"><Plus /></div>
|
||||
<hr>
|
||||
<div class="info-container"><h2>Add Account</h2></div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.add-account {
|
||||
width: 480px;
|
||||
height: var(--sp-xxxl);
|
||||
padding: .5em;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
background-color: color-mix(in srgb, var(--on-primary), var(--bg-1) 33%);
|
||||
border: 1px solid var(--on-primary);
|
||||
border-radius: var(--radius-0);
|
||||
|
||||
box-shadow: 0 var(--sp-ssm) var(--sp-sm) #0001;
|
||||
|
||||
transition: background-color .1s, border-color .1s;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.add-account:hover {
|
||||
background-color: color-mix(in srgb, var(--on-primary), var(--bg-1) 20%);
|
||||
border-color: var(--on-primary);
|
||||
}
|
||||
|
||||
.add-account:active:not(:has(.email:active)) {
|
||||
background-color: var(--on-primary);
|
||||
}
|
||||
|
||||
.icon-container {
|
||||
width: var(--sp-xxxl);
|
||||
min-width: var(--sp-xxxl);
|
||||
height: var(--sp-xxxl);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.icon-container :global(svg) {
|
||||
--size: 60%;
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
}
|
||||
|
||||
.add-account hr {
|
||||
margin: 0 .5em;
|
||||
border: none;
|
||||
border-left: 1px solid var(--on-primary);
|
||||
}
|
||||
|
||||
.info-container {
|
||||
width: 100%;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.info-container h2 {
|
||||
margin-left: .2em;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,25 +1,37 @@
|
|||
<script lang="ts">
|
||||
import { Account } from '@jupiter/lib/account';
|
||||
import { User } from '@lucide/svelte';
|
||||
import { type Account } from '@jupiter/lib/account';
|
||||
import { pushToast, ToastType } from '@jupiter/lib/toasts';
|
||||
import { Copy, User } from '@lucide/svelte';
|
||||
|
||||
let {
|
||||
account,
|
||||
} = $props<{
|
||||
account: Account,
|
||||
}>();
|
||||
|
||||
async function copyEmail() {
|
||||
await navigator.clipboard.write([new ClipboardItem({
|
||||
"text/plain": `${account.username}@${account.domain}`,
|
||||
})]);
|
||||
|
||||
pushToast("Email copied to clipboard.", ToastType.SUCCESS);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="account">
|
||||
<div class="account" role="button">
|
||||
<div class="icon-container"><User /></div>
|
||||
<hr>
|
||||
<div class="info-container">
|
||||
<h2 class="name">{account.username}</h2>
|
||||
<p class="email">{account.username}@{account.domain}</p>
|
||||
<button class="email" onclick={() => {copyEmail()}}>
|
||||
{account.username}@{account.domain}<Copy />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.account {
|
||||
width: 480px;
|
||||
height: var(--sp-xxxl);
|
||||
padding: .5em;
|
||||
|
||||
|
|
@ -30,6 +42,8 @@
|
|||
border: 1px solid var(--bg-2);
|
||||
border-radius: var(--radius-0);
|
||||
|
||||
box-shadow: 0 var(--sp-ssm) var(--sp-sm) #0001;
|
||||
|
||||
transition: background-color .1s, border-color .1s;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
|
@ -97,6 +111,10 @@
|
|||
font-size: 1.2em;
|
||||
line-height: 1.6em;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
color: var(--on-bg-3);
|
||||
background-color: var(--bg-1);
|
||||
border: 1px solid var(--bg-2);
|
||||
|
|
@ -114,4 +132,9 @@
|
|||
.email:active {
|
||||
background-color: var(--bg-3);
|
||||
}
|
||||
|
||||
.info-container .email :global(svg) {
|
||||
height: 1em;
|
||||
margin-left: var(--sp-sm);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
19
src/web/frontend/src/components/ui/Card.svelte
Normal file
19
src/web/frontend/src/components/ui/Card.svelte
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<div class="card"><slot></slot></div>
|
||||
|
||||
<style>
|
||||
.card {
|
||||
width: 480px;
|
||||
min-height: var(--sp-xxxl);
|
||||
padding: 1em;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
background-color: var(--bg-0);
|
||||
border: 1px solid var(--bg-2);
|
||||
border-radius: var(--radius-0);
|
||||
box-shadow: 0 var(--sp-ssm) var(--sp-sm) #0001;
|
||||
|
||||
user-select: none;
|
||||
}
|
||||
</style>
|
||||
45
src/web/frontend/src/components/ui/SidebarLink.svelte
Normal file
45
src/web/frontend/src/components/ui/SidebarLink.svelte
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
<script lang="ts">
|
||||
let {
|
||||
children,
|
||||
href,
|
||||
} = $props<{
|
||||
children: () => any,
|
||||
href: string
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<a href="{href}">{@render children()}</a>
|
||||
|
||||
<style>
|
||||
a {
|
||||
margin-left: var(--sp-sm);
|
||||
padding: .5em;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: start;
|
||||
align-items: center;
|
||||
gap: .5em;
|
||||
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
|
||||
color: var(--on-bg-2);
|
||||
background-color: var(--bg-0);
|
||||
text-decoration: none;
|
||||
border: 1px solid var(--bg-2);
|
||||
border-radius: var(--radius-0);
|
||||
|
||||
box-shadow: calc(0px - var(--sp-sm)) 0 0 var(--on-primary);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
background-color: var(--bg-1);
|
||||
}
|
||||
|
||||
a:active {
|
||||
background-color: var(--bg-2);
|
||||
}
|
||||
</style>
|
||||
37
src/web/frontend/src/components/ui/Skeleton.svelte
Normal file
37
src/web/frontend/src/components/ui/Skeleton.svelte
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
<div class="skeleton"><slot></slot></div>
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--width: 500px;
|
||||
}
|
||||
|
||||
.skeleton {
|
||||
border: 1px solid var(--bg-2);
|
||||
border-radius: var(--sp-sm);
|
||||
|
||||
background-image: linear-gradient(
|
||||
90deg,
|
||||
var(--bg-1),
|
||||
var(--bg-0),
|
||||
var(--bg-1)
|
||||
);
|
||||
background-size: var(--width) 100%;
|
||||
background-repeat: repeat;
|
||||
|
||||
animation: skeleton 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes throb {
|
||||
from { opacity: 1 }
|
||||
to { opacity: .25 }
|
||||
}
|
||||
|
||||
@keyframes skeleton {
|
||||
from {
|
||||
background-position: 0 0;
|
||||
}
|
||||
to {
|
||||
background-position: var(--width) 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { X } from '@lucide/svelte';
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { fade, fly } from "svelte/transition";
|
||||
import { fly } from "svelte/transition";
|
||||
import { ToastTypeToString } from '@jupiter/lib/toasts';
|
||||
|
||||
let {
|
||||
|
|
@ -19,7 +19,7 @@
|
|||
const dispatch = createEventDispatcher();
|
||||
</script>
|
||||
|
||||
<div class={["notification", ToastTypeToString(type)]} in:fly={{ x: 100 }} out:fade>
|
||||
<div class={["notification", ToastTypeToString(type)]} in:fly={{ x: 100 }}>
|
||||
<span class="content">{text}</span>
|
||||
{#if sticky}
|
||||
<div class="close"><X onclick={() => {dispatch("dismiss")}} /></div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
export interface Account {
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
export default interface Account {
|
||||
username: string,
|
||||
domain: string,
|
||||
mail_directory: string,
|
||||
}
|
||||
|
||||
export let accounts = writable<Account[] | null>(null);
|
||||
|
|
|
|||
|
|
@ -19,25 +19,21 @@ export const DEFAULT_NOTIFICATION_TIMEOUT = 5000;
|
|||
|
||||
export const toasts = writable<Toast[]>([]);
|
||||
|
||||
export function pushToast(toast: Record<string, any>) {
|
||||
export function pushToast(
|
||||
text: string,
|
||||
type: ToastType,
|
||||
sticky: boolean = false,
|
||||
timeout: number = DEFAULT_NOTIFICATION_TIMEOUT,
|
||||
) {
|
||||
const id = Math.floor(Math.random() * 1_000_000);
|
||||
const defaults: Toast = {
|
||||
id,
|
||||
text: "",
|
||||
type: ToastType.INFO,
|
||||
sticky: false,
|
||||
timeout: DEFAULT_NOTIFICATION_TIMEOUT,
|
||||
};
|
||||
let toast: Toast = { id, text, type, sticky, timeout };
|
||||
|
||||
toast = { ...defaults, ...toast };
|
||||
console.log(`adding toast ${toast.id} (timeout ${toast.timeout})...`);
|
||||
toasts.update((toasts) => [...toasts, { ...defaults, ...toast }]);
|
||||
toasts.update((toasts) => [...toasts, toast]);
|
||||
|
||||
if (!toast.sticky) setTimeout(() => { dismissToast(id) }, toast.timeout);
|
||||
}
|
||||
|
||||
export function dismissToast(id: number) {
|
||||
console.log(`dismissing toast ${id}...`);
|
||||
toasts.update((toasts) => toasts.filter((toast) => toast.id !== id));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
import { LayoutDashboard, Mail, Settings, User } from '@lucide/svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import ToastOverlay from '@jupiter/components/ui/ToastOverlay.svelte';
|
||||
import SidebarLink from '@jupiter/components/ui/SidebarLink.svelte';
|
||||
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
|
@ -15,9 +16,9 @@
|
|||
<div class="app">
|
||||
<header class="sidebar">
|
||||
<h1><Mail /> Jupiter Mail</h1>
|
||||
<a href="/"><LayoutDashboard /> Dashboard</a>
|
||||
<a href="/accounts"><User /> Accounts</a>
|
||||
<a href="/settings"><Settings /> Settings</a>
|
||||
<SidebarLink href="/"><LayoutDashboard /> Dashboard</SidebarLink>
|
||||
<SidebarLink href="/accounts"><User /> Accounts</SidebarLink>
|
||||
<SidebarLink href="/settings"><Settings /> Settings</SidebarLink>
|
||||
</header>
|
||||
<main>
|
||||
{@render children()}
|
||||
|
|
@ -60,36 +61,4 @@
|
|||
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.sidebar a {
|
||||
margin-left: var(--sp-sm);
|
||||
padding: .5em;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: start;
|
||||
align-items: center;
|
||||
gap: .5em;
|
||||
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
|
||||
color: var(--on-primary);
|
||||
background-color: var(--bg-0);
|
||||
text-decoration: none;
|
||||
border: 1px solid var(--bg-2);
|
||||
border-radius: var(--radius-0);
|
||||
|
||||
box-shadow: calc(0px - var(--sp-sm)) 0 0 var(--on-primary);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.sidebar a:hover {
|
||||
background-color: var(--bg-1);
|
||||
}
|
||||
|
||||
.sidebar a:active {
|
||||
background-color: var(--bg-2);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,16 @@
|
|||
<script lang="ts">
|
||||
import { LayoutDashboard } from '@lucide/svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { LayoutDashboard, User } from '@lucide/svelte';
|
||||
import Card from '@jupiter/components/ui/Card.svelte';
|
||||
import Header from '@jupiter/components/ui/Header.svelte';
|
||||
import * as api from '@jupiter/lib/api';
|
||||
import { type Account, accounts } from '@jupiter/lib/account';
|
||||
|
||||
onMount(async () => {
|
||||
if ($accounts == null) api.fetchAccounts().then(accs => {
|
||||
accounts.update(() => accs);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="page-container">
|
||||
|
|
@ -8,6 +18,13 @@
|
|||
<LayoutDashboard />
|
||||
<h1>Dashboard</h1>
|
||||
</Header>
|
||||
|
||||
<main>
|
||||
<Card>
|
||||
<h2><a href="/accounts">Accounts</a></h2>
|
||||
<p class="active-accounts">Active: {$accounts.length}</p>
|
||||
</Card>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
|
@ -16,4 +33,26 @@
|
|||
|
||||
background-color: var(--bg-0);
|
||||
}
|
||||
|
||||
main {
|
||||
margin: 0;
|
||||
padding: 2em;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 1em;
|
||||
}
|
||||
|
||||
h1, h2, p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.active-accounts {
|
||||
margin-top: .5em;
|
||||
}
|
||||
|
||||
h2 a {
|
||||
color: inherit;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,19 +1,23 @@
|
|||
<script lang="ts">
|
||||
import { User } from '@lucide/svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import * as api from '@jupiter/lib/api';
|
||||
import { type Account, accounts } from '@jupiter/lib/account';
|
||||
import { pushToast, ToastType } from '@jupiter/lib/toasts';
|
||||
import Header from '@jupiter/components/ui/Header.svelte';
|
||||
import AccountListItem from '@jupiter/components/ui/AccountListItem.svelte';
|
||||
import { type Account } from '@jupiter/lib/account';
|
||||
import * as api from '@jupiter/lib/api';
|
||||
import { pushToast, ToastType } from '@jupiter/lib/toasts';
|
||||
|
||||
let accounts: Account[] = $state([]);
|
||||
import AccountAddButton from '@jupiter/components/ui/AccountAddButton.svelte';
|
||||
import Skeleton from '@jupiter/components/ui/Skeleton.svelte';
|
||||
|
||||
onMount(async () => {
|
||||
pushToast({ text: "Fetching accounts...", type: ToastType.INFO });
|
||||
accounts = await api.fetchAccounts();
|
||||
pushToast({ text: "Accounts loaded successfully.", type: ToastType.SUCCESS });
|
||||
if ($accounts == null) api.fetchAccounts().then(accs => {
|
||||
accounts.update(() => accs);
|
||||
});
|
||||
});
|
||||
|
||||
function addAccount() {
|
||||
alert("TODO: this");
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="page-container">
|
||||
|
|
@ -26,9 +30,21 @@
|
|||
<p>Click an account below to configure:</p>
|
||||
<hr>
|
||||
<div class="account-list">
|
||||
{#each accounts as account}
|
||||
{#if $accounts}
|
||||
{#each $accounts as account}
|
||||
<AccountListItem account={account} />
|
||||
{/each}
|
||||
<AccountAddButton
|
||||
onclick={() => { addAccount() }}
|
||||
onkeydown={(event: KeyboardEvent) => {
|
||||
if (event.key === "Enter") addAccount()
|
||||
}}
|
||||
/>
|
||||
{:else}
|
||||
{#each {length: 4} as i}
|
||||
<Skeleton><div class="account-skeleton"></div></Skeleton>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
|
@ -48,8 +64,15 @@
|
|||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 1em;
|
||||
}
|
||||
|
||||
.account-skeleton {
|
||||
width: 480px;
|
||||
height: var(--sp-xxxl);
|
||||
padding: .5em;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"moduleResolution": "nodenext"
|
||||
"moduleResolution": "bundler"
|
||||
}
|
||||
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
|
||||
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue