feat: redesign routed ZarinPal merchant dashboard

This commit is contained in:
2026-08-20 21:41:18 +03:30
parent d8eeff57cd
commit 9ce2e6ec83
183 changed files with 7991 additions and 1569 deletions

View File

@@ -0,0 +1,207 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/state';
import ActivityIcon from '@lucide/svelte/icons/activity';
import ArrowUpDownIcon from '@lucide/svelte/icons/arrow-up-down';
import BookOpenIcon from '@lucide/svelte/icons/book-open';
import ChartNoAxesCombinedIcon from '@lucide/svelte/icons/chart-no-axes-combined';
import HandCoinsIcon from '@lucide/svelte/icons/hand-coins';
import LogOutIcon from '@lucide/svelte/icons/log-out';
import MenuIcon from '@lucide/svelte/icons/menu';
import MoonIcon from '@lucide/svelte/icons/moon';
import PanelsTopLeftIcon from '@lucide/svelte/icons/panels-top-left';
import SunIcon from '@lucide/svelte/icons/sun';
import UsersRoundIcon from '@lucide/svelte/icons/users-round';
import WalletCardsIcon from '@lucide/svelte/icons/wallet-cards';
import BrandLogo from '$lib/components/BrandLogo.svelte';
import { Button } from '$lib/components/ui/button';
import * as Sidebar from '$lib/components/ui/sidebar';
import { useDashboardContext } from '$lib/dashboard-context.svelte';
import { setDashboardQuery } from '$lib/dashboard-filters';
import { formatValue } from '$lib/format';
let { children } = $props();
const dashboard = useDashboardContext();
let dark = $state(document.documentElement.dataset.theme === 'dark');
const items = [
{ href: '/dashboard', label: 'پیشخوان', icon: PanelsTopLeftIcon },
{ href: '/transactions', label: 'تراکنش‌ها', icon: ArrowUpDownIcon },
{ href: '/payments', label: 'عملکرد پرداخت', icon: ActivityIcon },
{ href: '/fees', label: 'کارمزدها', icon: HandCoinsIcon },
{ href: '/peers', label: 'هم‌گروه‌ها', icon: UsersRoundIcon },
];
function navUrl(path: string) {
const url = new URL(page.url);
url.pathname = path;
for (const key of [...url.searchParams.keys()]) {
if (!['range', 'terminal'].includes(key)) url.searchParams.delete(key);
}
return `${url.pathname}${url.search}`;
}
function isActive(path: string) {
return (
page.url.pathname === path ||
(path === '/transactions' && page.url.pathname.startsWith('/transactions/'))
);
}
function terminalChanged(event: Event) {
const next = setDashboardQuery(page.url, {
terminal: (event.currentTarget as HTMLSelectElement).value,
});
void goto(`${next.pathname}${next.search}`, { keepFocus: true, noScroll: true });
}
function toggleTheme() {
dark = !dark;
document.documentElement.dataset.theme = dark ? 'dark' : 'light';
document.documentElement.classList.toggle('dark', dark);
localStorage.setItem('zpa-theme', dark ? 'dark' : 'light');
}
async function logout() {
await dashboard.logout();
await goto('/');
}
</script>
<Sidebar.Provider class="dashboard-shell" style="--sidebar-width: 16rem; --sidebar-width-icon: 4.5rem;">
<Sidebar.Root side="right" collapsible="icon" class="top-14 h-[calc(100svh-3.5rem)] border-l">
<Sidebar.Header class="border-sidebar-border gap-2 border-b p-2.5">
<div class="terminal-card">
<WalletCardsIcon class="size-5 shrink-0" />
<div class="min-w-0 group-data-[collapsible=icon]:hidden">
<strong class="block truncate text-sm">{dashboard.session?.merchant.displayName}</strong>
<span class="block truncate text-[11px] text-white/75">حساب تحلیل پذیرنده</span>
</div>
</div>
<div class="group-data-[collapsible=icon]:hidden">
<label class="text-muted-foreground mb-1 block text-[11px]" for="sidebar-terminal-select"
>درگاه فعال گزارش</label
>
<select
id="sidebar-terminal-select"
value={page.url.searchParams.get('terminal') ?? 'all'}
onchange={terminalChanged}
class="border-input bg-background focus:ring-ring/30 h-9 w-full rounded-md border px-2 text-xs outline-none focus:ring-2"
>
<option value="all">همه درگاه‌ها</option>
{#each dashboard.terminals as terminal}
<option value={terminal.terminalId}
>{terminal.displayName} · {formatValue(terminal.sessionCount)}</option
>
{/each}
</select>
</div>
</Sidebar.Header>
<Sidebar.Content class="px-2 py-3">
<Sidebar.Group>
<Sidebar.GroupContent>
<Sidebar.Menu class="gap-1">
{#each items as item}
<Sidebar.MenuItem>
<Sidebar.MenuButton
isActive={isActive(item.href)}
tooltipContent={item.label}
class="data-[active=true]:text-primary h-10 justify-start px-3 text-[13px]"
onclick={() => goto(navUrl(item.href))}
>
<item.icon class="size-[19px]" strokeWidth={1.8} />
<span>{item.label}</span>
</Sidebar.MenuButton>
</Sidebar.MenuItem>
{/each}
</Sidebar.Menu>
</Sidebar.GroupContent>
</Sidebar.Group>
<Sidebar.Separator class="my-2" />
<Sidebar.Group>
<Sidebar.GroupContent>
<Sidebar.Menu>
<Sidebar.MenuItem>
<Sidebar.MenuButton
isActive={isActive('/methodology')}
tooltipContent="روش محاسبه"
class="data-[active=true]:text-primary h-10 justify-start px-3 text-[13px]"
onclick={() => goto(navUrl('/methodology'))}
>
<BookOpenIcon class="size-[19px]" strokeWidth={1.8} />
<span>روش محاسبه</span>
</Sidebar.MenuButton>
</Sidebar.MenuItem>
</Sidebar.Menu>
</Sidebar.GroupContent>
</Sidebar.Group>
</Sidebar.Content>
<Sidebar.Footer class="border-sidebar-border border-t p-2">
<Sidebar.Menu>
<Sidebar.MenuItem>
<Sidebar.MenuButton tooltipContent="خروج" class="h-10 px-3" onclick={logout}>
<LogOutIcon class="size-[18px]" />
<span>خروج از حساب</span>
</Sidebar.MenuButton>
</Sidebar.MenuItem>
</Sidebar.Menu>
</Sidebar.Footer>
<Sidebar.Rail />
</Sidebar.Root>
<div
class="bg-card fixed inset-x-0 top-0 z-30 flex h-14 items-center border-b px-4 shadow-[0_1px_2px_rgba(0,0,0,.03)]"
>
<div class="flex w-full items-center justify-between">
<div class="flex items-center gap-2">
<Sidebar.Trigger class="size-9" aria-label="باز و بسته کردن منو"><MenuIcon /></Sidebar.Trigger>
<BrandLogo />
</div>
<div class="flex items-center gap-1">
<label class="sr-only" for="mobile-terminal-select">درگاه فعال گزارش</label>
<select
id="mobile-terminal-select"
value={page.url.searchParams.get('terminal') ?? 'all'}
onchange={terminalChanged}
class="border-input bg-background h-8 max-w-36 rounded-md border px-2 text-xs md:hidden"
>
<option value="all">همه درگاه‌ها</option>
{#each dashboard.terminals as terminal}
<option value={terminal.terminalId}>{terminal.displayName}</option>
{/each}
</select>
<Button variant="ghost" size="icon" onclick={toggleTheme} aria-label="تغییر پوسته">
{#if dark}<SunIcon />{:else}<MoonIcon />{/if}
</Button>
<span class="bg-border mx-1 h-6 w-px"></span>
<div class="grid size-8 place-items-center rounded-full bg-zinc-900 text-xs font-bold text-white">
{dashboard.session?.user.username.slice(0, 1).toUpperCase()}
</div>
</div>
</div>
</div>
<Sidebar.Inset class="bg-background mt-14 min-w-0">
<main class="mx-auto w-full max-w-[1220px] px-4 py-6 sm:px-6 lg:px-8 lg:py-8">
{@render children()}
</main>
</Sidebar.Inset>
</Sidebar.Provider>
<style>
:global(.dashboard-shell) {
direction: rtl;
}
.terminal-card {
display: flex;
min-height: 52px;
align-items: center;
gap: 10px;
overflow: hidden;
border-radius: 7px;
background: var(--primary);
padding: 10px;
color: var(--primary-foreground);
}
</style>