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';
2025-05-23 16:26:02 +03:00
import { Badge } from '$lib/components/ui/badge';
2025-05-26 15:06:45 +03:00
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,
2025-05-23 16:26:02 +03:00
LogOutIcon,
2025-05-24 15:50:10 +03:00
Wallet,
2025-05-26 15:06:45 +03:00
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,
2025-05-28 16:44:30 +03:00
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';
2025-06-11 11:06:05 +03:00
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';
2025-05-26 13:05:47 +03:00
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';
2025-05-26 15:06:45 +03:00
import { formatValue , getPublicUrl } from '$lib/utils';
2025-05-23 16:26:02 +03:00
import { goto } from '$app/navigation';
2025-05-28 16:44:30 +03:00
import { liveTradesStore , isLoadingTrades } from '$lib/stores/websocket';
2025-06-11 11:06:05 +03:00
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 } ,
2025-05-28 16:44:30 +03:00
{ title : 'Hopium' , url : '/hopium' , icon : TrendingUpDown } ,
{ title : 'Gambling' , url : '/gambling' , icon : PiggyBank } ,
2025-05-24 15:50:10 +03:00
{ title : 'Leaderboard' , url : '/leaderboard' , icon : Trophy } ,
2025-05-28 16:44:30 +03:00
{ 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
2025-06-11 11:06:05 +03:00
onMount(() => {
2025-05-23 16:26:02 +03:00
if ($USER_DATA) {
2025-06-11 11:06:05 +03:00
fetchPortfolioSummary();
2025-05-23 16:26:02 +03:00
} else {
2025-06-11 11:06:05 +03:00
PORTFOLIO_SUMMARY.set(null);
2025-05-23 16:26:02 +03:00
}
});
2025-05-22 13:17:11 +03:00
function handleNavClick(title: string) {
setOpenMobile(false);
}
function handleModeToggle() {
setMode(mode.current === 'light' ? 'dark' : 'light');
setOpenMobile(false);
}
2025-05-23 16:26:02 +03:00
function formatCurrency(value: number): string {
return value.toLocaleString('en-US', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
});
}
2025-05-26 15:06:45 +03:00
function handleLiveTradesClick() {
goto('/live');
setOpenMobile(false);
}
2025-06-11 10:35:02 +03:00
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 } );
}
2025-05-26 15:06:45 +03:00
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 >
2025-05-26 15:06:45 +03:00
< / Sidebar.GroupContent >
< / Sidebar.Group >
2025-05-26 13:05:47 +03:00
<!-- Daily Rewards -->
{ #if $USER_DATA }
< Sidebar.Group >
< Sidebar.GroupContent >
< div class = "px-2 py-1" >
2025-06-11 11:06:05 +03:00
{ #if ! $PORTFOLIO_SUMMARY }
2025-05-26 15:06:45 +03:00
< div class = "space-y-2" >
< Skeleton class = "h-8 w-full rounded" / >
< / div >
{ : else }
< DailyRewards / >
{ /if }
2025-05-26 13:05:47 +03:00
< / div >
< / Sidebar.GroupContent >
< / Sidebar.Group >
{ /if }
2025-05-23 16:26:02 +03:00
2025-05-26 15:06:45 +03:00
<!-- 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
2025-06-11 10:35:02 +03:00
onclick={() => handleTradeClick ( trade . coinSymbol , trade )}
2025-05-26 15:06:45 +03:00
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" >
2025-06-08 21:58:40 +03:00
{ #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' }
2025-05-26 15:06:45 +03:00
< 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 >
2025-06-08 21:58:40 +03:00
{ #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 }
2025-05-26 15:06:45 +03:00
< span class = "text-muted-foreground" > @{ trade . username } </ span >
< / div >
< / div >
< / button >
{ /each }
{ /if }
< / div >
< / Sidebar.GroupContent >
< / Sidebar.Group >
2025-05-23 16:26:02 +03:00
<!-- Portfolio Summary -->
2025-05-26 15:06:45 +03:00
{ #if $USER_DATA }
2025-05-23 16:26:02 +03:00
< Sidebar.Group >
< Sidebar.GroupLabel > Portfolio< / Sidebar.GroupLabel >
< Sidebar.GroupContent >
2025-05-26 15:06:45 +03:00
< div class = "space-y-2 px-2 py-1" >
2025-06-11 11:06:05 +03:00
{ #if ! $PORTFOLIO_SUMMARY }
2025-05-26 15:06:45 +03:00
< 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" / >
2025-05-23 16:26:02 +03:00
< / div >
2025-05-26 15:06:45 +03:00
< 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 >
2025-05-23 16:26:02 +03:00
< / div >
2025-05-26 15:06:45 +03:00
{ : 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" >
2025-06-11 11:06:05 +03:00
${ formatCurrency ( $PORTFOLIO_SUMMARY . totalValue )}
2025-05-26 15:06:45 +03:00
< / Badge >
< / div >
< div class = "text-muted-foreground space-y-1 text-xs" >
< div class = "flex justify-between" >
< span > Cash:< / span >
< span class = "font-mono"
2025-06-11 11:06:05 +03:00
>${ formatCurrency ( $PORTFOLIO_SUMMARY . baseCurrencyBalance )} < /span
2025-05-26 15:06:45 +03:00
>
< / div >
< div class = "flex justify-between" >
< span > Coins:< / span >
2025-06-11 11:06:05 +03:00
< span class = "font-mono" > ${ formatCurrency ( $PORTFOLIO_SUMMARY . totalCoinValue )} </ span >
2025-05-26 15:06:45 +03:00
< / div >
2025-05-23 16:26:02 +03:00
< / div >
2025-05-26 15:06:45 +03:00
{ /if }
2025-05-23 16:26:02 +03:00
< / 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" >
2025-05-23 16:26:02 +03:00
< 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 >
2025-05-23 16:26:02 +03:00
< 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
2025-05-22 14:06:21 +03:00
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" >
2025-05-23 16:26:02 +03:00
< 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 >
2025-05-23 16:26:02 +03:00
< 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 >
2025-05-28 16:44:30 +03:00
< 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 })}
2025-05-31 14:02:10 +03:00
< 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 })}
2025-05-31 14:02:10 +03:00
< 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 >