This repository has been archived on 2025-08-19. You can view files and clone it, but you cannot make any changes to its state, such as pushing and creating new issues, pull requests or comments.
coinstorge/website/src/lib/components/self/AppSidebar.svelte

517 lines
15 KiB
Svelte
Raw Normal View History

2025-05-22 13:17:11 +03:00
<script lang="ts">
import * as Sidebar from '$lib/components/ui/sidebar';
2025-05-22 14:00:43 +03:00
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
import * as Avatar from '$lib/components/ui/avatar';
import { Badge } from '$lib/components/ui/badge';
import { Skeleton } from '$lib/components/ui/skeleton';
2025-05-22 13:17:11 +03:00
import {
Moon,
Sun,
Home,
Store,
BriefcaseBusiness,
2025-05-22 14:00:43 +03:00
Coins,
ChevronsUpDownIcon,
LogOutIcon,
Wallet,
Trophy,
Activity,
TrendingUp,
2025-05-26 17:20:53 +03:00
TrendingDown,
User,
Settings,
Gift,
Shield,
2025-05-27 14:12:29 +03:00
Ticket,
PiggyBank,
ChartColumn,
2025-05-30 12:10:33 +03:00
TrendingUpDown,
Scale,
2025-05-31 19:29:20 +03:00
ShieldCheck,
2025-06-10 20:00:31 +03:00
Hammer,
BookOpen
2025-05-22 13:17:11 +03:00
} from 'lucide-svelte';
import { mode, setMode } from 'mode-watcher';
import type { HTMLAttributes } from 'svelte/elements';
import { USER_DATA } from '$lib/stores/user-data';
import { PORTFOLIO_SUMMARY, fetchPortfolioSummary } from '$lib/stores/portfolio-data';
2025-05-22 13:17:11 +03:00
import { useSidebar } from '$lib/components/ui/sidebar/index.js';
import SignInConfirmDialog from './SignInConfirmDialog.svelte';
import DailyRewards from './DailyRewards.svelte';
2025-05-26 17:20:53 +03:00
import PromoCodeDialog from './PromoCodeDialog.svelte';
2025-06-10 20:00:31 +03:00
import UserManualModal from './UserManualModal.svelte';
2025-05-22 14:00:43 +03:00
import { signOut } from '$lib/auth-client';
import { formatValue, getPublicUrl } from '$lib/utils';
import { goto } from '$app/navigation';
import { liveTradesStore, isLoadingTrades } from '$lib/stores/websocket';
import { onMount } from 'svelte';
2025-05-22 13:17:11 +03:00
const data = {
navMain: [
{ title: 'Home', url: '/', icon: Home },
{ title: 'Market', url: '/market', icon: Store },
{ title: 'Hopium', url: '/hopium', icon: TrendingUpDown },
{ title: 'Gambling', url: '/gambling', icon: PiggyBank },
{ title: 'Leaderboard', url: '/leaderboard', icon: Trophy },
{ title: 'Portfolio', url: '/portfolio', icon: BriefcaseBusiness },
{ title: 'Treemap', url: '/treemap', icon: ChartColumn },
2025-05-22 13:17:11 +03:00
{ title: 'Create coin', url: '/coin/create', icon: Coins }
2025-05-26 17:20:53 +03:00
]
2025-05-22 13:17:11 +03:00
};
type MenuButtonProps = HTMLAttributes<HTMLAnchorElement | HTMLButtonElement>;
2025-05-22 14:00:43 +03:00
const { setOpenMobile, isMobile } = useSidebar();
2025-05-22 13:17:11 +03:00
let shouldSignIn = $state(false);
2025-05-26 17:20:53 +03:00
let showPromoCode = $state(false);
2025-06-10 20:00:31 +03:00
let showUserManual = $state(false);
2025-05-22 13:17:11 +03:00
onMount(() => {
if ($USER_DATA) {
fetchPortfolioSummary();
} else {
PORTFOLIO_SUMMARY.set(null);
}
});
2025-05-22 13:17:11 +03:00
function handleNavClick(title: string) {
setOpenMobile(false);
}
function handleModeToggle() {
setMode(mode.current === 'light' ? 'dark' : 'light');
setOpenMobile(false);
}
function formatCurrency(value: number): string {
return value.toLocaleString('en-US', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
});
}
function handleLiveTradesClick() {
goto('/live');
setOpenMobile(false);
}
async function handleTradeClick(coinSymbol: string, trade: any) {
if (trade.type === 'TRANSFER_IN' || trade.type === 'TRANSFER_OUT') {
const targetPath = `/user/${trade.username}`;
await goto(targetPath, { invalidateAll: true });
} else {
const targetPath = `/coin/${coinSymbol.toLowerCase()}`;
await goto(targetPath, { invalidateAll: true });
}
setOpenMobile(false);
}
2025-05-26 17:20:53 +03:00
function handleAccountClick() {
if ($USER_DATA) {
goto(`/user/${$USER_DATA.id}`);
setOpenMobile(false);
}
}
function handleSettingsClick() {
goto('/settings');
setOpenMobile(false);
}
function handleAdminClick() {
goto('/admin');
setOpenMobile(false);
}
2025-05-31 19:29:20 +03:00
function handleUserManagementClick() {
goto('/admin/users');
setOpenMobile(false);
}
2025-05-26 17:20:53 +03:00
function handlePromoCodesClick() {
goto('/admin/promo');
setOpenMobile(false);
}
2025-05-30 12:10:33 +03:00
function handleTermsClick() {
goto('/legal/terms');
setOpenMobile(false);
}
function handlePrivacyClick() {
goto('/legal/privacy');
setOpenMobile(false);
}
2025-06-10 20:00:31 +03:00
function handleUserManualClick() {
showUserManual = true;
setOpenMobile(false);
}
2025-05-22 13:17:11 +03:00
</script>
2025-05-22 14:00:43 +03:00
<SignInConfirmDialog bind:open={shouldSignIn} />
2025-05-26 17:20:53 +03:00
<PromoCodeDialog bind:open={showPromoCode} />
2025-06-10 20:00:31 +03:00
<UserManualModal bind:open={showUserManual} />
2025-05-22 13:17:11 +03:00
<Sidebar.Root collapsible="offcanvas">
<Sidebar.Header>
2025-05-30 13:50:06 +03:00
<div class="flex items-center gap-2 px-2 py-2">
<img src="/rugplay.svg" class="h-5 w-5" alt="twoblade" />
2025-05-22 13:17:11 +03:00
<div class="flex items-center gap-2">
<span class="text-base font-semibold">Rugplay</span>
{#if $USER_DATA?.isAdmin}
<span class="text-muted-foreground text-xs">| Admin</span>
{/if}
</div>
</div>
</Sidebar.Header>
<Sidebar.Content>
<Sidebar.Group>
<Sidebar.GroupContent>
<Sidebar.Menu>
{#each data.navMain as item}
<Sidebar.MenuItem>
<Sidebar.MenuButton>
{#snippet child({ props }: { props: MenuButtonProps })}
<a
href={item.url || '/'}
onclick={() => handleNavClick(item.title)}
class={`${props.class}`}
>
<item.icon />
<span>{item.title}</span>
</a>
{/snippet}
</Sidebar.MenuButton>
</Sidebar.MenuItem>
{/each}
<Sidebar.MenuItem>
<Sidebar.MenuButton>
{#snippet child({ props }: { props: MenuButtonProps })}
<button onclick={handleModeToggle} {...props}>
{#if mode.current === 'light'}
<Moon class="h-5 w-5" />
<span>Dark Mode</span>
{:else}
<Sun class="h-5 w-5" />
<span>Light Mode</span>
{/if}
</button>
{/snippet}
</Sidebar.MenuButton>
</Sidebar.MenuItem>
</Sidebar.Menu>
</Sidebar.GroupContent>
</Sidebar.Group>
<!-- Daily Rewards -->
{#if $USER_DATA}
<Sidebar.Group>
<Sidebar.GroupContent>
<div class="px-2 py-1">
{#if !$PORTFOLIO_SUMMARY}
<div class="space-y-2">
<Skeleton class="h-8 w-full rounded" />
</div>
{:else}
<DailyRewards />
{/if}
</div>
</Sidebar.GroupContent>
</Sidebar.Group>
{/if}
<!-- Live Trades -->
<Sidebar.Group>
<Sidebar.GroupLabel class="flex items-center justify-between">
<div class="flex items-center gap-2">
<Activity class="h-4 w-4" />
<span>Live Trades</span>
</div>
<button
onclick={handleLiveTradesClick}
class="text-muted-foreground hover:text-foreground cursor-pointer text-xs transition-colors"
>
View All
</button>
</Sidebar.GroupLabel>
<Sidebar.GroupContent>
<div class="space-y-1 px-2 py-1">
{#if $isLoadingTrades}
{#each Array(5) as _, i}
<div class="flex items-center gap-2 py-1 text-xs">
<div class="flex items-center gap-1">
<Skeleton class="h-3 w-3 rounded-full" />
<Skeleton class="h-4 w-8" />
</div>
<div class="flex-1">
<div class="flex items-center gap-1">
<Skeleton class="h-3 w-12" />
<Skeleton class="h-3 w-28" />
</div>
</div>
</div>
{/each}
{:else if $liveTradesStore.length === 0}
<div class="text-muted-foreground py-2 text-center text-xs">No big trades yet...</div>
{:else}
{#each $liveTradesStore.slice(0, 5) as trade, index (`${trade.timestamp}-${trade.username}-${trade.coinSymbol}-${index}`)}
<button
onclick={() => handleTradeClick(trade.coinSymbol, trade)}
class="hover:bg-muted/50 flex w-full cursor-pointer items-center gap-2 rounded px-1 py-1 text-left text-xs transition-colors"
>
<div class="flex items-center gap-1">
{#if trade.type === 'TRANSFER_IN' || trade.type === 'TRANSFER_OUT'}
<Activity class="h-3 w-3 text-blue-500" />
<Badge
variant="outline"
class="h-4 border-blue-500 px-1 py-0 text-[10px] text-blue-500"
>
{trade.type === 'TRANSFER_IN' ? 'REC' : 'SENT'}
</Badge>
{:else if trade.type === 'BUY'}
<TrendingUp class="h-3 w-3 text-green-500" />
<Badge
variant="outline"
class="h-4 border-green-500 px-1 py-0 text-[10px] text-green-500"
>
BUY
</Badge>
{:else}
<TrendingDown class="h-3 w-3 text-red-500" />
<Badge
variant="outline"
class="h-4 border-red-500 px-1 py-0 text-[10px] text-red-500"
>
SELL
</Badge>
{/if}
</div>
<div class="flex-1 truncate">
<div class="flex items-center gap-1">
<span class="text-foreground font-medium">
{formatValue(trade.totalValue)}
</span>
{#if trade.type === 'TRANSFER_IN' || trade.type === 'TRANSFER_OUT'}
{#if trade.amount > 0}
<span class="text-muted-foreground">*{trade.coinSymbol}</span>
{/if}
<span class="text-muted-foreground">
{trade.type === 'TRANSFER_IN' ? 'to' : 'from'}
</span>
{:else}
<span class="text-muted-foreground">*{trade.coinSymbol}</span>
<span class="text-muted-foreground">by</span>
{/if}
<span class="text-muted-foreground">@{trade.username}</span>
</div>
</div>
</button>
{/each}
{/if}
</div>
</Sidebar.GroupContent>
</Sidebar.Group>
<!-- Portfolio Summary -->
{#if $USER_DATA}
<Sidebar.Group>
<Sidebar.GroupLabel>Portfolio</Sidebar.GroupLabel>
<Sidebar.GroupContent>
<div class="space-y-2 px-2 py-1">
{#if !$PORTFOLIO_SUMMARY}
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<Skeleton class="h-4 w-4 rounded" />
<Skeleton class="h-4 w-16" />
</div>
<Skeleton class="h-5 w-16 rounded" />
</div>
<div class="space-y-1">
<div class="flex justify-between">
<Skeleton class="h-3 w-8" />
<Skeleton class="h-3 w-12" />
</div>
<div class="flex justify-between">
<Skeleton class="h-3 w-10" />
<Skeleton class="h-3 w-12" />
</div>
</div>
{:else}
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<Wallet class="text-muted-foreground h-4 w-4" />
<span class="text-sm font-medium">Total Value</span>
</div>
<Badge variant="secondary" class="font-mono">
${formatCurrency($PORTFOLIO_SUMMARY.totalValue)}
</Badge>
</div>
<div class="text-muted-foreground space-y-1 text-xs">
<div class="flex justify-between">
<span>Cash:</span>
<span class="font-mono"
>${formatCurrency($PORTFOLIO_SUMMARY.baseCurrencyBalance)}</span
>
</div>
<div class="flex justify-between">
<span>Coins:</span>
<span class="font-mono">${formatCurrency($PORTFOLIO_SUMMARY.totalCoinValue)}</span>
</div>
</div>
{/if}
</div>
</Sidebar.GroupContent>
</Sidebar.Group>
{/if}
2025-05-22 13:17:11 +03:00
</Sidebar.Content>
2025-05-22 14:00:43 +03:00
{#if $USER_DATA}
<Sidebar.Footer>
<Sidebar.Menu>
<Sidebar.MenuItem>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
{#snippet child({ props })}
<Sidebar.MenuButton
size="lg"
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
{...props}
>
<Avatar.Root class="size-8 rounded-lg">
<Avatar.Image src={getPublicUrl($USER_DATA.image)} alt={$USER_DATA.name} />
<Avatar.Fallback class="rounded-lg">?</Avatar.Fallback>
2025-05-22 14:00:43 +03:00
</Avatar.Root>
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-medium">{$USER_DATA.name}</span>
<span class="truncate text-xs">@{$USER_DATA.username}</span>
2025-05-22 14:00:43 +03:00
</div>
<ChevronsUpDownIcon class="ml-auto size-4" />
</Sidebar.MenuButton>
{/snippet}
</DropdownMenu.Trigger>
<DropdownMenu.Content
class="w-(--bits-dropdown-menu-anchor-width) min-w-56 rounded-lg p-2"
2025-05-22 14:00:43 +03:00
side={isMobile ? 'bottom' : 'right'}
align="end"
sideOffset={4}
>
<DropdownMenu.Label class="p-0 font-normal">
<div class="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<Avatar.Root class="size-8 rounded-lg">
<Avatar.Image src={getPublicUrl($USER_DATA.image)} alt={$USER_DATA.name} />
<Avatar.Fallback class="rounded-lg">?</Avatar.Fallback>
2025-05-22 14:00:43 +03:00
</Avatar.Root>
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-medium">{$USER_DATA.name}</span>
<span class="truncate text-xs">@{$USER_DATA.username}</span>
2025-05-22 14:00:43 +03:00
</div>
</div>
</DropdownMenu.Label>
<DropdownMenu.Separator />
<DropdownMenu.Group>
2025-05-26 17:20:53 +03:00
<DropdownMenu.Item onclick={handleAccountClick}>
<User />
2025-05-22 14:00:43 +03:00
Account
</DropdownMenu.Item>
2025-05-26 17:20:53 +03:00
<DropdownMenu.Item onclick={handleSettingsClick}>
<Settings />
Settings
2025-05-22 14:00:43 +03:00
</DropdownMenu.Item>
2025-06-10 20:00:31 +03:00
<DropdownMenu.Item onclick={handleUserManualClick}>
<BookOpen />
User Manual
</DropdownMenu.Item>
<DropdownMenu.Item
onclick={() => {
showPromoCode = true;
setOpenMobile(false);
}}
>
2025-05-26 17:20:53 +03:00
<Gift />
Promo code
2025-05-22 14:00:43 +03:00
</DropdownMenu.Item>
</DropdownMenu.Group>
2025-05-30 12:10:33 +03:00
2025-05-26 17:20:53 +03:00
{#if $USER_DATA?.isAdmin}
<DropdownMenu.Separator />
<DropdownMenu.Group>
<DropdownMenu.Item
onclick={handleAdminClick}
class="text-primary hover:text-primary!"
>
<Shield class="text-primary" />
Admin Panel
</DropdownMenu.Item>
2025-05-31 19:29:20 +03:00
<DropdownMenu.Item
onclick={handleUserManagementClick}
class="text-primary hover:text-primary!"
>
<Hammer class="text-primary" />
User Management
</DropdownMenu.Item>
2025-05-26 17:20:53 +03:00
<DropdownMenu.Item
onclick={handlePromoCodesClick}
class="text-primary hover:text-primary!"
>
<Ticket class="text-primary" />
Manage codes
</DropdownMenu.Item>
</DropdownMenu.Group>
{/if}
2025-05-30 12:10:33 +03:00
<DropdownMenu.Group>
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={handleTermsClick}>
<Scale />
Terms of Service
</DropdownMenu.Item>
<DropdownMenu.Item onclick={handlePrivacyClick}>
<ShieldCheck />
Privacy Policy
</DropdownMenu.Item>
</DropdownMenu.Group>
2025-05-22 14:00:43 +03:00
<DropdownMenu.Separator />
<DropdownMenu.Item
onclick={() => {
signOut().then(() => {
USER_DATA.set(null);
window.location.reload();
});
}}
>
<LogOutIcon />
Log out
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
</Sidebar.MenuItem>
</Sidebar.Menu>
</Sidebar.Footer>
2025-05-30 12:10:33 +03:00
{:else}
<Sidebar.Footer>
<Sidebar.Menu>
<Sidebar.MenuItem>
<Sidebar.MenuButton>
{#snippet child({ props }: { props: MenuButtonProps })}
<a href="/legal/terms" onclick={handleTermsClick} class={`${props.class}`}>
2025-05-30 12:10:33 +03:00
<Scale />
<span>Terms of Service</span>
</a>
{/snippet}
</Sidebar.MenuButton>
</Sidebar.MenuItem>
<Sidebar.MenuItem>
<Sidebar.MenuButton>
{#snippet child({ props }: { props: MenuButtonProps })}
<a href="/legal/privacy" onclick={handlePrivacyClick} class={`${props.class}`}>
2025-05-30 12:10:33 +03:00
<ShieldCheck />
<span>Privacy Policy</span>
</a>
{/snippet}
</Sidebar.MenuButton>
</Sidebar.MenuItem>
</Sidebar.Menu>
</Sidebar.Footer>
2025-05-22 14:00:43 +03:00
{/if}
2025-05-22 13:17:11 +03:00
</Sidebar.Root>