npub.email/src/components/MailboxFolderItems.svelte
Danny Morabito 6ee0809661
clean up code and ui
- IconButton component
- make FoldersListSidebar a separate component from the index page
- make move letter to folder better looking
- clean up code on index page
2024-11-29 14:09:41 +01:00

328 lines
8.2 KiB
Svelte

<script lang="ts">
import { groupByStamps, sortBy } from '$lib/stores.svelte';
import Icon from '@iconify/svelte';
import NostrIdentifier from './NostrIdentifier.svelte';
import { getReadableDate, getReadableTime, moveMessageToFolder } from '$lib/utils.svelte.js';
import Dialog from './Dialog.svelte';
import Select from './Select.svelte';
import type { Letter } from '$lib/letter';
import { slide } from 'svelte/transition';
import IconButton from './IconButton.svelte';
let {
letters,
foldersList = $bindable<{ id: string; name: string; icon: string }[]>(),
folder = $bindable<{ id: string; name: string; icon: string }>()
} = $props();
function formatRange(start, end) {
if (start === 0) return '0';
if (end === Infinity) return `${start}+`;
return `${start}-${end}`;
}
let sortedInbox = $derived(
letters.toSorted((a, b) => {
switch ($sortBy) {
case 'stamps':
return b.stamps - a.stamps;
case 'sender':
return a.from.localeCompare(b.from);
case 'subject':
return a.subject.localeCompare(b.subject);
case 'date':
return b.date - a.date;
}
return 0;
})
);
let groupedInbox = $derived.by(() => {
const maxStamps = Math.max(...sortedInbox.map(letter => letter.stamps));
const ranges = [
10000,
1000,
210,
100,
42,
21,
10,
0
];
if (!ranges.includes(maxStamps)) {
ranges.push(maxStamps);
ranges.sort((a, b) => b - a);
}
const groups = {};
for (let i = 0; i < ranges.length; i++) {
const start = ranges[i];
const end = ranges[i + 1] || Infinity;
const rangeKey = formatRange(start, end);
groups[rangeKey] = [];
}
sortedInbox.forEach(letter => {
for (let i = 0; i < ranges.length; i++) {
const start = ranges[i];
const end = ranges[i + 1] || Infinity;
if (letter.stamps >= start && letter.stamps < end) {
const rangeKey = formatRange(start, end);
groups[rangeKey].push(letter);
break;
}
}
});
return Object.entries(groups)
.filter(([_, letters]) => letters.length > 0)
.reverse();
});
let selectedLetters = $state<string[]>([]);
let moveFolderDialogOpen = $state(false);
let moveFolderName = $state('');
function clickedLetter(letter: Letter) {
if (selectedLetters.includes(letter.id))
selectedLetters = selectedLetters.filter(id => id !== letter.id);
else
selectedLetters = [...selectedLetters, letter.id];
}
async function doMove() {
if (!moveFolderName) return alert('Please select a folder');
if (selectedLetters.length === 0) return alert('Please select at least one letter');
const events = [];
for (let letter of selectedLetters)
events.push(await moveMessageToFolder(
letter,
moveFolderName
));
for (let event of events)
await event.publish();
selectedLetters = [];
moveFolderName = '';
moveFolderDialogOpen = false;
window.location.reload();
}
</script>
{#snippet letterGroup(letters)}
<div class="letter-group">
{#each letters as letter}
{#key letter.id}
<a in:slide out:slide class="letter-item" href="/letters/{letter.id}">
<div class="subject-line">
<div class="letter-subject">
{#if !letter.emailAddress}
<img src="/nostr-lock.svg" width="32" />
{/if}
{letter.subject}
</div>
{#if letter.stamps}
<div class="stamps-count">
<Icon icon="emojione-v1:stamped-envelope" />
{letter.stamps} sats
</div>
{/if}
</div>
<div class="letter-from">
{#if folder.id === 'sent'}
{#each letter.recipients as recipient}
<NostrIdentifier user={recipient.npub} />
{/each}
{:else}
<NostrIdentifier user={letter.from.npub} emailAddress={letter.emailAddress} />
{/if}
</div>
<div class="letter-meta">
<Icon icon="mdi:calendar" />
{getReadableDate(letter.date)}
&nbsp;
<Icon icon="mdi:clock-outline" />
{getReadableTime(letter.date)}
</div>
<div class="letter-preview">{letter.preview}</div>
</a>
{/key}
{/each}
</div>
{/snippet}
<Dialog
bind:open={moveFolderDialogOpen}
>
<div class="title">
<h3>Move Letters to Folder</h3>
</div>
<p class="text-secondary">Select a folder to move the selected letters to:</p>
<Select bind:value={moveFolderName} label="Select folder" options={
foldersList.filter(cf => cf.id !== folder.id && cf.id != 'sent').map(folder => ({ value: folder.id, label: folder.name }))
} />
<h4>Letters ({selectedLetters.length} selected)</h4>
<div class="letter-list">
<div class="letter-group">
{#each letters as letter}
{#key letter.id}
<div class="letter-item"
onclick={() => clickedLetter(letter)}
class:selected={selectedLetters.includes(letter.id)}
>
<div class="subject-line">
<div class="letter-subject">
<Icon icon="mdi:letter-outline" /> {letter.subject}
</div>
{#if letter.stamps}
<div class="stamps-count">
<Icon icon="emojione-v1:stamped-envelope" />
{letter.stamps} sats
</div>
{/if}
</div>
<div class="letter-from">
<NostrIdentifier user={letter.from.npub} emailAddress={letter.emailAddress} />
</div>
</div>
{/key}
{/each}
</div>
</div>
<div class="button-group">
<button onclick={() => moveFolderDialogOpen = false} type="button">Cancel</button>
<button onclick={doMove} type="submit">OK</button>
</div>
</Dialog>
<h2 id="page-title">
{folder.name}
</h2>
<div class="actions">
<IconButton icon="mdi:folder-move-outline" onclick={() => moveFolderDialogOpen = true} text="Move Letters" />
</div>
<main class="letter-list">
{#if $groupByStamps}
{#each groupedInbox as [range, letters]}
<div class="stamp-group">
{#if range == 0}
<h3>No stamps</h3>
{:else}
<h3>{range} sats stamp</h3>
{/if}
<div class="letter-group">
{@render letterGroup(letters)}
</div>
</div>
{/each}
{:else}
{@render letterGroup(sortedInbox)}
{/if}
</main>
<style>
@layer layout {
.letter-list {
display: grid;
gap: var(--spacing-sm);
overflow: hidden;
.stamp-group {
display: grid;
gap: var(--spacing-md);
}
.letter-group {
display: grid;
gap: var(--spacing-sm);
}
.letter-meta {
color: var(--text-secondary);
font-size: 0.85rem;
margin-bottom: var(--spacing-xs);
display: flex;
align-items: center;
gap: var(--spacing-xs);
}
& .letter-item {
text-decoration: none;
color: var(--text-primary);
position: relative;
overflow: hidden;
padding: var(--spacing-md);
border-radius: 12px;
background: color-mix(in srgb, var(--glass-bg) 80%, transparent);
border: 1px solid var(--glass-border);
transition: all 0.3s ease;
cursor: pointer;
&::before {
content: '';
position: absolute;
inset: 0;
background: var(--gradient-shine);
background-size: 200% 200%;
opacity: 0;
transition: opacity 0.3s ease;
}
&:hover, &.selected {
transform: translateY(-2px) scale(1.01);
background: color-mix(in srgb, var(--glass-bg) 90%, var(--accent));
box-shadow: 0 4px 16px var(--glass-shadow);
&::before {
opacity: 1;
animation: shine 2s linear infinite;
}
}
& .subject-line {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-xs);
}
& .letter-subject, .letter-from {
display: flex;
flex-wrap: wrap;
flex-direction: row;
align-content: center;
align-items: center;
font-weight: 600;
}
& .stamps-count {
color: var(--text-secondary);
font-size: 0.85rem;
display: flex;
align-items: center;
gap: var(--spacing-xs);
}
& .letter-preview {
color: var(--text-secondary);
font-size: 0.9rem;
}
}
}
.actions {
display: flex;
align-items: center;
gap: var(--spacing-sm);
margin-bottom: var(--spacing-sm);
}
}
</style>