feat: treemap, skeletons
This commit is contained in:
parent
848eda70e4
commit
330ea7ad79
18 changed files with 1033 additions and 45 deletions
179
website/package-lock.json
generated
179
website/package-lock.json
generated
|
|
@ -13,6 +13,7 @@
|
||||||
"@tailwindcss/postcss": "^4.1.7",
|
"@tailwindcss/postcss": "^4.1.7",
|
||||||
"@tailwindcss/typography": "^0.5.16",
|
"@tailwindcss/typography": "^0.5.16",
|
||||||
"@visx/scale": "^3.12.0",
|
"@visx/scale": "^3.12.0",
|
||||||
|
"apexcharts": "^4.7.0",
|
||||||
"better-auth": "^1.2.8",
|
"better-auth": "^1.2.8",
|
||||||
"drizzle-orm": "^0.33.0",
|
"drizzle-orm": "^0.33.0",
|
||||||
"ioredis": "^5.6.1",
|
"ioredis": "^5.6.1",
|
||||||
|
|
@ -20,6 +21,7 @@
|
||||||
"lucide-svelte": "^0.511.0",
|
"lucide-svelte": "^0.511.0",
|
||||||
"mode-watcher": "^1.0.7",
|
"mode-watcher": "^1.0.7",
|
||||||
"postgres": "^3.4.4",
|
"postgres": "^3.4.4",
|
||||||
|
"svelte-apexcharts": "^1.0.2",
|
||||||
"svelte-lightweight-charts": "^2.2.0"
|
"svelte-lightweight-charts": "^2.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
@ -1972,6 +1974,57 @@
|
||||||
"vite": "^5.0.0"
|
"vite": "^5.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@svgdotjs/svg.draggable.js": {
|
||||||
|
"version": "3.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@svgdotjs/svg.draggable.js/-/svg.draggable.js-3.0.6.tgz",
|
||||||
|
"integrity": "sha512-7iJFm9lL3C40HQcqzEfezK2l+dW2CpoVY3b77KQGqc8GXWa6LhhmX5Ckv7alQfUXBuZbjpICZ+Dvq1czlGx7gA==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@svgdotjs/svg.js": "^3.2.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@svgdotjs/svg.filter.js": {
|
||||||
|
"version": "3.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@svgdotjs/svg.filter.js/-/svg.filter.js-3.0.9.tgz",
|
||||||
|
"integrity": "sha512-/69XMRCDoam2HgC4ldHIaDgeQf1ViHIsa0Ld4uWgiXtZ+E24DWHe/9Ib6kbNiZ7WRIdlVokUDR1Fg0kjIpkfbw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@svgdotjs/svg.js": "^3.2.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@svgdotjs/svg.js": {
|
||||||
|
"version": "3.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@svgdotjs/svg.js/-/svg.js-3.2.4.tgz",
|
||||||
|
"integrity": "sha512-BjJ/7vWNowlX3Z8O4ywT58DqbNRyYlkk6Yz/D13aB7hGmfQTvGX4Tkgtm/ApYlu9M7lCQi15xUEidqMUmdMYwg==",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/Fuzzyma"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@svgdotjs/svg.resize.js": {
|
||||||
|
"version": "2.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@svgdotjs/svg.resize.js/-/svg.resize.js-2.0.5.tgz",
|
||||||
|
"integrity": "sha512-4heRW4B1QrJeENfi7326lUPYBCevj78FJs8kfeDxn5st0IYPIRXoTtOSYvTzFWgaWWXd3YCDE6ao4fmv91RthA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14.18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@svgdotjs/svg.js": "^3.2.4",
|
||||||
|
"@svgdotjs/svg.select.js": "^4.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@svgdotjs/svg.select.js": {
|
||||||
|
"version": "4.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@svgdotjs/svg.select.js/-/svg.select.js-4.0.3.tgz",
|
||||||
|
"integrity": "sha512-qkMgso1sd2hXKd1FZ1weO7ANq12sNmQJeGDjs46QwDVsxSRcHmvWKL2NDF7Yimpwf3sl5esOLkPqtV2bQ3v/Jg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14.18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@svgdotjs/svg.js": "^3.2.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@swc/helpers": {
|
"node_modules/@swc/helpers": {
|
||||||
"version": "0.5.17",
|
"version": "0.5.17",
|
||||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz",
|
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz",
|
||||||
|
|
@ -2396,6 +2449,11 @@
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@yr/monotone-cubic-spline": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@yr/monotone-cubic-spline/-/monotone-cubic-spline-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA=="
|
||||||
|
},
|
||||||
"node_modules/acorn": {
|
"node_modules/acorn": {
|
||||||
"version": "8.14.1",
|
"version": "8.14.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|
@ -2406,6 +2464,19 @@
|
||||||
"node": ">=0.4.0"
|
"node": ">=0.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/apexcharts": {
|
||||||
|
"version": "4.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-4.7.0.tgz",
|
||||||
|
"integrity": "sha512-iZSrrBGvVlL+nt2B1NpqfDuBZ9jX61X9I2+XV0hlYXHtTwhwLTHDKGXjNXAgFBDLuvSYCB/rq2nPWVPRv2DrGA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@svgdotjs/svg.draggable.js": "^3.0.4",
|
||||||
|
"@svgdotjs/svg.filter.js": "^3.0.8",
|
||||||
|
"@svgdotjs/svg.js": "^3.2.4",
|
||||||
|
"@svgdotjs/svg.resize.js": "^2.0.2",
|
||||||
|
"@svgdotjs/svg.select.js": "^4.0.1",
|
||||||
|
"@yr/monotone-cubic-spline": "^1.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/aria-query": {
|
"node_modules/aria-query": {
|
||||||
"version": "5.3.2",
|
"version": "5.3.2",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
|
@ -3905,6 +3976,31 @@
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/svelte-apexcharts": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/svelte-apexcharts/-/svelte-apexcharts-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-6qlx4rE+XsonZ0FZudfwqOQ34Pq+3wpxgAD75zgEmGoYhYBJcwmikTuTf3o8ZBsZue9U/pAwhNy3ed1Bkq1gmA==",
|
||||||
|
"dependencies": {
|
||||||
|
"apexcharts": "^3.19.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"apexcharts": "^3.19.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/svelte-apexcharts/node_modules/apexcharts": {
|
||||||
|
"version": "3.54.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.54.1.tgz",
|
||||||
|
"integrity": "sha512-E4et0h/J1U3r3EwS/WlqJCQIbepKbp6wGUmaAwJOMjHUP4Ci0gxanLa7FR3okx6p9coi4st6J853/Cb1NP0vpA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@yr/monotone-cubic-spline": "^1.0.3",
|
||||||
|
"svg.draggable.js": "^2.2.2",
|
||||||
|
"svg.easing.js": "^2.0.0",
|
||||||
|
"svg.filter.js": "^2.0.2",
|
||||||
|
"svg.pathmorphing.js": "^0.1.3",
|
||||||
|
"svg.resize.js": "^1.4.3",
|
||||||
|
"svg.select.js": "^3.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/svelte-check": {
|
"node_modules/svelte-check": {
|
||||||
"version": "4.2.1",
|
"version": "4.2.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
|
@ -3984,6 +4080,89 @@
|
||||||
"svelte": "^5.0.0"
|
"svelte": "^5.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/svg.draggable.js": {
|
||||||
|
"version": "2.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/svg.draggable.js/-/svg.draggable.js-2.2.2.tgz",
|
||||||
|
"integrity": "sha512-JzNHBc2fLQMzYCZ90KZHN2ohXL0BQJGQimK1kGk6AvSeibuKcIdDX9Kr0dT9+UJ5O8nYA0RB839Lhvk4CY4MZw==",
|
||||||
|
"dependencies": {
|
||||||
|
"svg.js": "^2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/svg.easing.js": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/svg.easing.js/-/svg.easing.js-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-//ctPdJMGy22YoYGV+3HEfHbm6/69LJUTAqI2/5qBvaNHZ9uUFVC82B0Pl299HzgH13rKrBgi4+XyXXyVWWthA==",
|
||||||
|
"dependencies": {
|
||||||
|
"svg.js": ">=2.3.x"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/svg.filter.js": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/svg.filter.js/-/svg.filter.js-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-xkGBwU+dKBzqg5PtilaTb0EYPqPfJ9Q6saVldX+5vCRy31P6TlRCP3U9NxH3HEufkKkpNgdTLBJnmhDHeTqAkw==",
|
||||||
|
"dependencies": {
|
||||||
|
"svg.js": "^2.2.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/svg.js": {
|
||||||
|
"version": "2.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/svg.js/-/svg.js-2.7.1.tgz",
|
||||||
|
"integrity": "sha512-ycbxpizEQktk3FYvn/8BH+6/EuWXg7ZpQREJvgacqn46gIddG24tNNe4Son6omdXCnSOaApnpZw6MPCBA1dODA=="
|
||||||
|
},
|
||||||
|
"node_modules/svg.pathmorphing.js": {
|
||||||
|
"version": "0.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/svg.pathmorphing.js/-/svg.pathmorphing.js-0.1.3.tgz",
|
||||||
|
"integrity": "sha512-49HWI9X4XQR/JG1qXkSDV8xViuTLIWm/B/7YuQELV5KMOPtXjiwH4XPJvr/ghEDibmLQ9Oc22dpWpG0vUDDNww==",
|
||||||
|
"dependencies": {
|
||||||
|
"svg.js": "^2.4.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/svg.resize.js": {
|
||||||
|
"version": "1.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/svg.resize.js/-/svg.resize.js-1.4.3.tgz",
|
||||||
|
"integrity": "sha512-9k5sXJuPKp+mVzXNvxz7U0uC9oVMQrrf7cFsETznzUDDm0x8+77dtZkWdMfRlmbkEEYvUn9btKuZ3n41oNA+uw==",
|
||||||
|
"dependencies": {
|
||||||
|
"svg.js": "^2.6.5",
|
||||||
|
"svg.select.js": "^2.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/svg.resize.js/node_modules/svg.select.js": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-tH6ABEyJsAOVAhwcCjF8mw4crjXSI1aa7j2VQR8ZuJ37H2MBUbyeqYr5nEO7sSN3cy9AR9DUwNg0t/962HlDbQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"svg.js": "^2.2.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/svg.select.js": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-h5IS/hKkuVCbKSieR9uQCj9w+zLHoPh+ce19bBYyqF53g6mnPB8sAtIbe1s9dh2S2fCmYX2xel1Ln3PJBbK4kw==",
|
||||||
|
"dependencies": {
|
||||||
|
"svg.js": "^2.6.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tabbable": {
|
"node_modules/tabbable": {
|
||||||
"version": "6.2.0",
|
"version": "6.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@
|
||||||
"@tailwindcss/postcss": "^4.1.7",
|
"@tailwindcss/postcss": "^4.1.7",
|
||||||
"@tailwindcss/typography": "^0.5.16",
|
"@tailwindcss/typography": "^0.5.16",
|
||||||
"@visx/scale": "^3.12.0",
|
"@visx/scale": "^3.12.0",
|
||||||
|
"apexcharts": "^4.7.0",
|
||||||
"better-auth": "^1.2.8",
|
"better-auth": "^1.2.8",
|
||||||
"drizzle-orm": "^0.33.0",
|
"drizzle-orm": "^0.33.0",
|
||||||
"ioredis": "^5.6.1",
|
"ioredis": "^5.6.1",
|
||||||
|
|
@ -51,6 +52,7 @@
|
||||||
"lucide-svelte": "^0.511.0",
|
"lucide-svelte": "^0.511.0",
|
||||||
"mode-watcher": "^1.0.7",
|
"mode-watcher": "^1.0.7",
|
||||||
"postgres": "^3.4.4",
|
"postgres": "^3.4.4",
|
||||||
|
"svelte-apexcharts": "^1.0.2",
|
||||||
"svelte-lightweight-charts": "^2.2.0"
|
"svelte-lightweight-charts": "^2.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,8 @@
|
||||||
Settings,
|
Settings,
|
||||||
Gift,
|
Gift,
|
||||||
Shield,
|
Shield,
|
||||||
Ticket
|
Ticket,
|
||||||
|
BarChart3
|
||||||
} from 'lucide-svelte';
|
} from 'lucide-svelte';
|
||||||
import { mode, setMode } from 'mode-watcher';
|
import { mode, setMode } from 'mode-watcher';
|
||||||
import type { HTMLAttributes } from 'svelte/elements';
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
|
|
@ -44,6 +45,7 @@
|
||||||
{ title: 'Market', url: '/market', icon: Store },
|
{ title: 'Market', url: '/market', icon: Store },
|
||||||
{ title: 'Portfolio', url: '/portfolio', icon: BriefcaseBusiness },
|
{ title: 'Portfolio', url: '/portfolio', icon: BriefcaseBusiness },
|
||||||
{ title: 'Leaderboard', url: '/leaderboard', icon: Trophy },
|
{ title: 'Leaderboard', url: '/leaderboard', icon: Trophy },
|
||||||
|
{ title: 'Treemap', url: '/treemap', icon: BarChart3 },
|
||||||
{ title: 'Create coin', url: '/coin/create', icon: Coins }
|
{ title: 'Create coin', url: '/coin/create', icon: Coins }
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import { Gift, Clock, CheckCircle, Flame, Loader2 } from 'lucide-svelte';
|
import { Gift, Clock, Loader2, CheckIcon } from 'lucide-svelte';
|
||||||
import { USER_DATA } from '$lib/stores/user-data';
|
import { USER_DATA } from '$lib/stores/user-data';
|
||||||
import { fetchPortfolioData } from '$lib/stores/portfolio-data';
|
import { fetchPortfolioData } from '$lib/stores/portfolio-data';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
|
|
@ -143,7 +143,7 @@
|
||||||
<Loader2 class="h-4 w-4 animate-spin" />
|
<Loader2 class="h-4 w-4 animate-spin" />
|
||||||
<span>{!rewardStatus ? 'Loading...' : 'Claiming...'}</span>
|
<span>{!rewardStatus ? 'Loading...' : 'Claiming...'}</span>
|
||||||
{:else if claimState === 'success'}
|
{:else if claimState === 'success'}
|
||||||
<CheckCircle class="h-4 w-4" />
|
<CheckIcon class="h-4 w-4" />
|
||||||
<span>Claimed!</span>
|
<span>Claimed!</span>
|
||||||
{:else if rewardStatus.canClaim}
|
{:else if rewardStatus.canClaim}
|
||||||
<Gift class="h-4 w-4" />
|
<Gift class="h-4 w-4" />
|
||||||
|
|
|
||||||
163
website/src/lib/components/self/skeletons/CoinSkeleton.svelte
Normal file
163
website/src/lib/components/self/skeletons/CoinSkeleton.svelte
Normal file
|
|
@ -0,0 +1,163 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import * as Card from '$lib/components/ui/card';
|
||||||
|
import { Skeleton } from '$lib/components/ui/skeleton';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<header class="mb-8">
|
||||||
|
<div class="mb-6 flex items-start justify-between">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<Skeleton class="h-16 w-16 rounded-lg" />
|
||||||
|
<div>
|
||||||
|
<Skeleton class="mb-2 h-10 w-48" />
|
||||||
|
<div class="mt-1 flex items-center gap-2">
|
||||||
|
<Skeleton class="h-6 w-16" />
|
||||||
|
<Skeleton class="h-6 w-20" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-right">
|
||||||
|
<Skeleton class="mb-2 h-8 w-32" />
|
||||||
|
<div class="mt-2 flex items-center gap-2">
|
||||||
|
<Skeleton class="h-4 w-4" />
|
||||||
|
<Skeleton class="h-6 w-16" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Creator Info Skeleton -->
|
||||||
|
<div class="text-muted-foreground flex items-center gap-2 text-sm">
|
||||||
|
<Skeleton class="h-4 w-20" />
|
||||||
|
<Skeleton class="h-4 w-4 rounded-full" />
|
||||||
|
<Skeleton class="h-4 w-40" />
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="grid gap-6">
|
||||||
|
<!-- Price Chart with Trading Actions -->
|
||||||
|
<div class="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
||||||
|
<!-- Chart (2/3 width) -->
|
||||||
|
<div class="lg:col-span-2">
|
||||||
|
<Card.Root>
|
||||||
|
<Card.Header class="pb-4">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<Card.Title class="flex items-center gap-2">
|
||||||
|
<Skeleton class="h-5 w-5" />
|
||||||
|
<Skeleton class="h-6 w-32" />
|
||||||
|
</Card.Title>
|
||||||
|
<div class="flex gap-1">
|
||||||
|
{#each Array(6) as _}
|
||||||
|
<Skeleton class="h-8 w-12" />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card.Header>
|
||||||
|
<Card.Content class="pt-0">
|
||||||
|
<Skeleton class="h-[500px] w-full" />
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right side - Trading Actions + Liquidity Pool (1/3 width) -->
|
||||||
|
<div class="space-y-6 lg:col-span-1">
|
||||||
|
<!-- Trading Actions Skeleton -->
|
||||||
|
<Card.Root>
|
||||||
|
<Card.Header class="pb-4">
|
||||||
|
<Card.Title>
|
||||||
|
<Skeleton class="h-6 w-24" />
|
||||||
|
</Card.Title>
|
||||||
|
<Skeleton class="h-4 w-32" />
|
||||||
|
</Card.Header>
|
||||||
|
<Card.Content class="pt-0">
|
||||||
|
<div class="space-y-3">
|
||||||
|
<Skeleton class="h-11 w-full" />
|
||||||
|
<Skeleton class="h-11 w-full" />
|
||||||
|
</div>
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
|
|
||||||
|
<!-- Liquidity Pool Skeleton -->
|
||||||
|
<Card.Root>
|
||||||
|
<Card.Header class="pb-4">
|
||||||
|
<Card.Title>
|
||||||
|
<Skeleton class="h-6 w-28" />
|
||||||
|
</Card.Title>
|
||||||
|
</Card.Header>
|
||||||
|
<Card.Content class="pt-0">
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<Skeleton class="mb-3 h-5 w-32" />
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<Skeleton class="h-4 w-16" />
|
||||||
|
<Skeleton class="h-4 w-20" />
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<Skeleton class="h-4 w-24" />
|
||||||
|
<Skeleton class="h-4 w-20" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Skeleton class="mb-3 h-5 w-20" />
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<Skeleton class="h-4 w-24" />
|
||||||
|
<Skeleton class="h-4 w-20" />
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<Skeleton class="h-4 w-20" />
|
||||||
|
<Skeleton class="h-4 w-20" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Statistics Grid Skeleton -->
|
||||||
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||||
|
{#each Array(4) as _}
|
||||||
|
<Card.Root>
|
||||||
|
<Card.Header class="pb-2">
|
||||||
|
<Card.Title class="flex items-center gap-2 text-sm font-medium">
|
||||||
|
<Skeleton class="h-4 w-4" />
|
||||||
|
<Skeleton class="h-4 w-20" />
|
||||||
|
</Card.Title>
|
||||||
|
</Card.Header>
|
||||||
|
<Card.Content class="pt-0">
|
||||||
|
<Skeleton class="mb-1 h-6 w-24" />
|
||||||
|
<Skeleton class="h-3 w-16" />
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Comments Section Skeleton -->
|
||||||
|
<Card.Root>
|
||||||
|
<Card.Header class="pb-4">
|
||||||
|
<Card.Title class="flex items-center gap-2">
|
||||||
|
<Skeleton class="h-5 w-5" />
|
||||||
|
<Skeleton class="h-6 w-20" />
|
||||||
|
</Card.Title>
|
||||||
|
</Card.Header>
|
||||||
|
<Card.Content class="pt-0">
|
||||||
|
<div class="space-y-4">
|
||||||
|
{#each Array(3) as _}
|
||||||
|
<div class="flex gap-3 border-b pb-4 last:border-b-0">
|
||||||
|
<Skeleton class="h-8 w-8 rounded-full" />
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="mb-2 flex items-center gap-2">
|
||||||
|
<Skeleton class="h-4 w-24" />
|
||||||
|
<Skeleton class="h-3 w-16" />
|
||||||
|
</div>
|
||||||
|
<Skeleton class="h-4 w-full" />
|
||||||
|
<Skeleton class="mt-1 h-4 w-3/4" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import * as Card from '$lib/components/ui/card';
|
||||||
|
import { Skeleton } from '$lib/components/ui/skeleton';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="container mx-auto p-6">
|
||||||
|
<!-- Header Skeleton -->
|
||||||
|
<header class="mb-8">
|
||||||
|
<Skeleton class="mb-2 h-9 w-64" />
|
||||||
|
<Skeleton class="h-5 w-96" />
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Top Coins Grid Skeleton -->
|
||||||
|
<div class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{#each Array(6) as _}
|
||||||
|
<Card.Root class="h-full">
|
||||||
|
<Card.Header>
|
||||||
|
<Card.Title class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Skeleton class="h-6 w-6 rounded-full" />
|
||||||
|
<Skeleton class="h-6 w-32" />
|
||||||
|
</div>
|
||||||
|
<Skeleton class="h-6 w-16" />
|
||||||
|
</Card.Title>
|
||||||
|
<Card.Description>
|
||||||
|
<Skeleton class="h-4 w-28" />
|
||||||
|
</Card.Description>
|
||||||
|
</Card.Header>
|
||||||
|
<Card.Content>
|
||||||
|
<div class="flex items-baseline justify-between">
|
||||||
|
<Skeleton class="h-9 w-24" />
|
||||||
|
<Skeleton class="h-4 w-20" />
|
||||||
|
</div>
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Market Overview Skeleton -->
|
||||||
|
<div class="mt-12">
|
||||||
|
<Skeleton class="mb-4 h-8 w-40" />
|
||||||
|
<Card.Root>
|
||||||
|
<Card.Content class="p-0">
|
||||||
|
<div class="p-6">
|
||||||
|
{#each Array(10) as _}
|
||||||
|
<div class="flex items-center gap-4 border-b py-4 last:border-b-0">
|
||||||
|
<Skeleton class="h-8 w-8 rounded-full" />
|
||||||
|
<div class="flex-1">
|
||||||
|
<Skeleton class="h-4 w-24" />
|
||||||
|
<Skeleton class="mt-1 h-3 w-16" />
|
||||||
|
</div>
|
||||||
|
<Skeleton class="h-4 w-16" />
|
||||||
|
<Skeleton class="h-4 w-16" />
|
||||||
|
<Skeleton class="h-4 w-20" />
|
||||||
|
<Skeleton class="h-4 w-20" />
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import * as Card from '$lib/components/ui/card';
|
||||||
|
import { Skeleton } from '$lib/components/ui/skeleton';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="grid gap-6 lg:grid-cols-2">
|
||||||
|
{#each Array(4) as _}
|
||||||
|
<Card.Root>
|
||||||
|
<Card.Header>
|
||||||
|
<Card.Title class="flex items-center gap-2">
|
||||||
|
<Skeleton class="h-6 w-6" />
|
||||||
|
<Skeleton class="h-6 w-40" />
|
||||||
|
</Card.Title>
|
||||||
|
<Card.Description>
|
||||||
|
<Skeleton class="h-4 w-64" />
|
||||||
|
</Card.Description>
|
||||||
|
</Card.Header>
|
||||||
|
<Card.Content>
|
||||||
|
<div class="space-y-4">
|
||||||
|
{#each Array(5) as _}
|
||||||
|
<div class="flex items-center gap-4 border-b pb-4 last:border-b-0">
|
||||||
|
<Skeleton class="h-6 w-8" />
|
||||||
|
<Skeleton class="h-8 w-8 rounded-full" />
|
||||||
|
<div class="flex-1">
|
||||||
|
<Skeleton class="h-4 w-24" />
|
||||||
|
<Skeleton class="mt-1 h-3 w-16" />
|
||||||
|
</div>
|
||||||
|
<Skeleton class="h-4 w-20" />
|
||||||
|
<Skeleton class="h-5 w-16" />
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import * as Card from '$lib/components/ui/card';
|
||||||
|
import { Skeleton } from '$lib/components/ui/skeleton';
|
||||||
|
import { MediaQuery } from 'svelte/reactivity';
|
||||||
|
|
||||||
|
const isDesktop = new MediaQuery('(min-width: 768px)');
|
||||||
|
let perPage = $derived(isDesktop.current ? 12 : 9);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Pagination Info Skeleton -->
|
||||||
|
<div class="mb-4 flex items-center justify-between">
|
||||||
|
<Skeleton class="h-5 w-48" />
|
||||||
|
<Skeleton class="h-5 w-24" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Market Grid Skeleton -->
|
||||||
|
<div class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||||
|
{#each Array(perPage) as _}
|
||||||
|
<Card.Root class="gap-1">
|
||||||
|
<Card.Header>
|
||||||
|
<div class="flex items-start justify-between">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<Skeleton class="h-8 w-8 rounded-full" />
|
||||||
|
<div>
|
||||||
|
<Skeleton class="mb-1 h-5 w-24" />
|
||||||
|
<Skeleton class="h-4 w-16" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-right">
|
||||||
|
<Skeleton class="h-3 w-8" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card.Header>
|
||||||
|
|
||||||
|
<Card.Content>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<!-- Price -->
|
||||||
|
<div>
|
||||||
|
<Skeleton class="mb-2 h-8 w-24" />
|
||||||
|
<div class="mt-1 flex items-center gap-2">
|
||||||
|
<Skeleton class="h-5 w-12" />
|
||||||
|
<Skeleton class="h-5 w-16" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Stats -->
|
||||||
|
<div class="space-y-4 text-sm">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<Skeleton class="h-4 w-16" />
|
||||||
|
<Skeleton class="h-4 w-12" />
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<Skeleton class="h-4 w-20" />
|
||||||
|
<Skeleton class="h-4 w-12" />
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<Skeleton class="h-4 w-12" />
|
||||||
|
<Skeleton class="h-4 w-16" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
128
website/src/lib/components/self/skeletons/ProfileSkeleton.svelte
Normal file
128
website/src/lib/components/self/skeletons/ProfileSkeleton.svelte
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import * as Card from '$lib/components/ui/card';
|
||||||
|
import { Skeleton } from '$lib/components/ui/skeleton';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Profile Header Skeleton -->
|
||||||
|
<Card.Root class="mb-6 py-0">
|
||||||
|
<Card.Content class="p-6">
|
||||||
|
<div class="flex flex-col gap-4 sm:flex-row sm:items-start">
|
||||||
|
<!-- Avatar Skeleton -->
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<Skeleton class="size-20 rounded-full sm:size-24" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Profile Info Skeleton -->
|
||||||
|
<div class="min-w-0 flex-1">
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="mb-1 flex flex-wrap items-center gap-2">
|
||||||
|
<Skeleton class="h-8 w-48 sm:h-9" />
|
||||||
|
<Skeleton class="h-6 w-16" />
|
||||||
|
</div>
|
||||||
|
<Skeleton class="h-6 w-32" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Skeleton class="mb-3 h-4 w-full max-w-2xl" />
|
||||||
|
<Skeleton class="mb-6 h-4 w-96" />
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Skeleton class="h-4 w-4" />
|
||||||
|
<Skeleton class="h-4 w-24" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
|
|
||||||
|
<!-- Main Portfolio Stats Skeleton -->
|
||||||
|
<div class="mb-6 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
|
{#each Array(4) as _}
|
||||||
|
<Card.Root class="py-0">
|
||||||
|
<Card.Content class="p-4">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<Skeleton class="h-4 w-24" />
|
||||||
|
<Skeleton class="h-4 w-4" />
|
||||||
|
</div>
|
||||||
|
<Skeleton class="mt-1 h-8 w-32" />
|
||||||
|
<Skeleton class="mt-1 h-3 w-20" />
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Buy & Sell Activity Skeleton -->
|
||||||
|
<div class="mb-6 grid grid-cols-1 gap-4 lg:grid-cols-4">
|
||||||
|
{#each Array(4) as _}
|
||||||
|
<Card.Root class="py-0">
|
||||||
|
<Card.Content class="p-4">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<Skeleton class="h-4 w-24" />
|
||||||
|
<Skeleton class="h-4 w-4" />
|
||||||
|
</div>
|
||||||
|
<div class="mt-1">
|
||||||
|
<Skeleton class="h-8 w-28" />
|
||||||
|
<Skeleton class="mt-1 h-3 w-24" />
|
||||||
|
</div>
|
||||||
|
<div class="border-muted mt-3 border-t pt-3">
|
||||||
|
<Skeleton class="h-6 w-24" />
|
||||||
|
<Skeleton class="mt-1 h-3 w-20" />
|
||||||
|
</div>
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Created Coins Skeleton -->
|
||||||
|
<Card.Root class="mb-6">
|
||||||
|
<Card.Header class="pb-3">
|
||||||
|
<Card.Title class="flex items-center gap-2">
|
||||||
|
<Skeleton class="h-5 w-5" />
|
||||||
|
<Skeleton class="h-6 w-32" />
|
||||||
|
</Card.Title>
|
||||||
|
<Skeleton class="h-4 w-48" />
|
||||||
|
</Card.Header>
|
||||||
|
<Card.Content class="p-0">
|
||||||
|
<div class="p-6">
|
||||||
|
{#each Array(3) as _}
|
||||||
|
<div class="flex items-center gap-4 border-b py-4 last:border-b-0">
|
||||||
|
<Skeleton class="h-8 w-8 rounded-full" />
|
||||||
|
<div class="flex-1">
|
||||||
|
<Skeleton class="h-4 w-24" />
|
||||||
|
<Skeleton class="mt-1 h-3 w-16" />
|
||||||
|
</div>
|
||||||
|
<Skeleton class="h-4 w-16" />
|
||||||
|
<Skeleton class="h-4 w-20" />
|
||||||
|
<Skeleton class="h-4 w-16" />
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
|
|
||||||
|
<!-- Recent Trading Activity Skeleton -->
|
||||||
|
<Card.Root>
|
||||||
|
<Card.Header class="pb-3">
|
||||||
|
<Card.Title class="flex items-center gap-2">
|
||||||
|
<Skeleton class="h-5 w-5" />
|
||||||
|
<Skeleton class="h-6 w-40" />
|
||||||
|
</Card.Title>
|
||||||
|
<Skeleton class="h-4 w-52" />
|
||||||
|
</Card.Header>
|
||||||
|
<Card.Content class="p-0">
|
||||||
|
<div class="p-6">
|
||||||
|
{#each Array(5) as _}
|
||||||
|
<div class="flex items-center gap-4 border-b py-4 last:border-b-0">
|
||||||
|
<Skeleton class="h-6 w-12" />
|
||||||
|
<Skeleton class="h-8 w-8 rounded-full" />
|
||||||
|
<div class="flex-1">
|
||||||
|
<Skeleton class="h-4 w-20" />
|
||||||
|
<Skeleton class="mt-1 h-3 w-16" />
|
||||||
|
</div>
|
||||||
|
<Skeleton class="h-4 w-16" />
|
||||||
|
<Skeleton class="h-4 w-16" />
|
||||||
|
<Skeleton class="h-4 w-20" />
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
|
|
@ -64,6 +64,16 @@ export function formatValue(value: number | string): string {
|
||||||
if (numValue >= 1e9) return `$${(numValue / 1e9).toFixed(2)}B`;
|
if (numValue >= 1e9) return `$${(numValue / 1e9).toFixed(2)}B`;
|
||||||
if (numValue >= 1e6) return `$${(numValue / 1e6).toFixed(2)}M`;
|
if (numValue >= 1e6) return `$${(numValue / 1e6).toFixed(2)}M`;
|
||||||
if (numValue >= 1e3) return `$${(numValue / 1e3).toFixed(2)}K`;
|
if (numValue >= 1e3) return `$${(numValue / 1e3).toFixed(2)}K`;
|
||||||
|
|
||||||
|
if (numValue < 0.01) {
|
||||||
|
const str = numValue.toString();
|
||||||
|
const match = str.match(/^0\.0*(\d{2})/);
|
||||||
|
if (match) {
|
||||||
|
const zerosAfterDecimal = str.indexOf(match[1]) - 2;
|
||||||
|
return `$${numValue.toFixed(zerosAfterDecimal + 2)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return `$${numValue.toFixed(2)}`;
|
return `$${numValue.toFixed(2)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
import SignInConfirmDialog from '$lib/components/self/SignInConfirmDialog.svelte';
|
import SignInConfirmDialog from '$lib/components/self/SignInConfirmDialog.svelte';
|
||||||
import CoinIcon from '$lib/components/self/CoinIcon.svelte';
|
import CoinIcon from '$lib/components/self/CoinIcon.svelte';
|
||||||
import DataTable from '$lib/components/self/DataTable.svelte';
|
import DataTable from '$lib/components/self/DataTable.svelte';
|
||||||
|
import HomeSkeleton from '$lib/components/self/skeletons/HomeSkeleton.svelte';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
|
@ -102,11 +103,7 @@
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<div class="flex h-96 items-center justify-center">
|
<HomeSkeleton />
|
||||||
<div class="text-center">
|
|
||||||
<div class="mb-4 text-xl">Loading market data...</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{:else if coins.length === 0}
|
{:else if coins.length === 0}
|
||||||
<div class="flex h-96 items-center justify-center">
|
<div class="flex h-96 items-center justify-center">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
import { Alert, AlertDescription } from '$lib/components/ui/alert';
|
import { Alert, AlertDescription } from '$lib/components/ui/alert';
|
||||||
import { Badge } from '$lib/components/ui/badge';
|
import { Badge } from '$lib/components/ui/badge';
|
||||||
import { Skeleton } from '$lib/components/ui/skeleton';
|
import { Skeleton } from '$lib/components/ui/skeleton';
|
||||||
import { Plus, Ticket, Users, Calendar, CheckCircle, XCircle, Loader2 } from 'lucide-svelte';
|
import { Plus, Ticket, Users, Calendar, XCircle, Loader2, CheckIcon } from 'lucide-svelte';
|
||||||
import { USER_DATA } from '$lib/stores/user-data';
|
import { USER_DATA } from '$lib/stores/user-data';
|
||||||
import { formatDate, getExpirationDate } from '$lib/utils';
|
import { formatDate, getExpirationDate } from '$lib/utils';
|
||||||
import type { PromoCode } from '$lib/types/promo-code';
|
import type { PromoCode } from '$lib/types/promo-code';
|
||||||
|
|
@ -216,7 +216,7 @@
|
||||||
class={createSuccess ? 'text-success' : ''}
|
class={createSuccess ? 'text-success' : ''}
|
||||||
>
|
>
|
||||||
{#if createSuccess}
|
{#if createSuccess}
|
||||||
<CheckCircle class="h-4 w-4 text-green-600" />
|
<CheckIcon class="h-4 w-4 text-green-600" />
|
||||||
{:else}
|
{:else}
|
||||||
<XCircle class="h-4 w-4" />
|
<XCircle class="h-4 w-4" />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -180,6 +180,13 @@ export async function POST({ params, request }) {
|
||||||
updatedAt: new Date()
|
updatedAt: new Date()
|
||||||
})
|
})
|
||||||
.where(eq(coin.id, coinData.id));
|
.where(eq(coin.id, coinData.id));
|
||||||
|
|
||||||
|
await redis.publish(`prices:${normalizedSymbol}`, JSON.stringify({
|
||||||
|
currentPrice: newPrice,
|
||||||
|
marketCap: Number(coinData.circulatingSupply) * newPrice,
|
||||||
|
change24h: metrics.change24h,
|
||||||
|
volume24h: metrics.volume24h
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
// REDIS
|
// REDIS
|
||||||
|
|
@ -313,6 +320,13 @@ export async function POST({ params, request }) {
|
||||||
updatedAt: new Date()
|
updatedAt: new Date()
|
||||||
})
|
})
|
||||||
.where(eq(coin.id, coinData.id));
|
.where(eq(coin.id, coinData.id));
|
||||||
|
|
||||||
|
await redis.publish(`prices:${normalizedSymbol}`, JSON.stringify({
|
||||||
|
currentPrice: newPrice,
|
||||||
|
marketCap: Number(coinData.circulatingSupply) * newPrice,
|
||||||
|
change24h: metrics.change24h,
|
||||||
|
volume24h: metrics.volume24h
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
// REDIS
|
// REDIS
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
import TradeModal from '$lib/components/self/TradeModal.svelte';
|
import TradeModal from '$lib/components/self/TradeModal.svelte';
|
||||||
import CommentSection from '$lib/components/self/CommentSection.svelte';
|
import CommentSection from '$lib/components/self/CommentSection.svelte';
|
||||||
import UserProfilePreview from '$lib/components/self/UserProfilePreview.svelte';
|
import UserProfilePreview from '$lib/components/self/UserProfilePreview.svelte';
|
||||||
|
import CoinSkeleton from '$lib/components/self/skeletons/CoinSkeleton.svelte';
|
||||||
import { TrendingUp, TrendingDown, DollarSign, Coins, ChartColumn } from 'lucide-svelte';
|
import { TrendingUp, TrendingDown, DollarSign, Coins, ChartColumn } from 'lucide-svelte';
|
||||||
import {
|
import {
|
||||||
createChart,
|
createChart,
|
||||||
|
|
@ -259,26 +260,17 @@
|
||||||
onSuccess={handleTradeSuccess}
|
onSuccess={handleTradeSuccess}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
<div class="container mx-auto max-w-7xl p-6">
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<div class="container mx-auto max-w-7xl p-6">
|
<CoinSkeleton />
|
||||||
<div class="flex h-96 items-center justify-center">
|
{:else if !coin}
|
||||||
<div class="text-center">
|
|
||||||
<div class="mb-4 text-xl">Loading coin data...</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{:else if !coin}
|
|
||||||
<div class="container mx-auto max-w-7xl p-6">
|
|
||||||
<div class="flex h-96 items-center justify-center">
|
<div class="flex h-96 items-center justify-center">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<div class="text-muted-foreground mb-4 text-xl">Coin not found</div>
|
<div class="text-muted-foreground mb-4 text-xl">Coin not found</div>
|
||||||
<Button onclick={() => goto('/')}>Go Home</Button>
|
<Button onclick={() => goto('/')}>Go Home</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{:else}
|
||||||
{:else}
|
|
||||||
<div class="container mx-auto max-w-7xl p-6">
|
|
||||||
<!-- Header Section -->
|
<!-- Header Section -->
|
||||||
<header class="mb-8">
|
<header class="mb-8">
|
||||||
<div class="mb-4 flex items-start justify-between">
|
<div class="mb-4 flex items-start justify-between">
|
||||||
|
|
@ -535,5 +527,5 @@
|
||||||
<!-- Comments Section -->
|
<!-- Comments Section -->
|
||||||
<CommentSection {coinSymbol} />
|
<CommentSection {coinSymbol} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
{/if}
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
import { Badge } from '$lib/components/ui/badge';
|
import { Badge } from '$lib/components/ui/badge';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import DataTable from '$lib/components/self/DataTable.svelte';
|
import DataTable from '$lib/components/self/DataTable.svelte';
|
||||||
|
import LeaderboardSkeleton from '$lib/components/self/skeletons/LeaderboardSkeleton.svelte';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
|
@ -215,11 +216,7 @@
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<div class="flex h-96 items-center justify-center">
|
<LeaderboardSkeleton />
|
||||||
<div class="text-center">
|
|
||||||
<div class="mb-4 text-xl">Loading leaderboard...</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{:else if !leaderboardData}
|
{:else if !leaderboardData}
|
||||||
<div class="flex h-96 items-center justify-center">
|
<div class="flex h-96 items-center justify-center">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
import { Badge } from '$lib/components/ui/badge';
|
import { Badge } from '$lib/components/ui/badge';
|
||||||
import { Label } from '$lib/components/ui/label';
|
import { Label } from '$lib/components/ui/label';
|
||||||
import CoinIcon from '$lib/components/self/CoinIcon.svelte';
|
import CoinIcon from '$lib/components/self/CoinIcon.svelte';
|
||||||
|
import MarketSkeleton from '$lib/components/self/skeletons/MarketSkeleton.svelte';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
|
@ -410,12 +411,7 @@
|
||||||
|
|
||||||
<!-- Market Grid -->
|
<!-- Market Grid -->
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<div class="flex h-96 items-center justify-center">
|
<MarketSkeleton />
|
||||||
<div class="text-center">
|
|
||||||
<div class="mb-4 text-xl">Loading market data...</div>
|
|
||||||
<div class="text-muted-foreground">Fetching the latest coin prices and chaos levels</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{:else if coins.length === 0}
|
{:else if coins.length === 0}
|
||||||
<div class="flex h-96 items-center justify-center">
|
<div class="flex h-96 items-center justify-center">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
|
|
|
||||||
350
website/src/routes/treemap/+page.svelte
Normal file
350
website/src/routes/treemap/+page.svelte
Normal file
|
|
@ -0,0 +1,350 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { chart } from 'svelte-apexcharts';
|
||||||
|
import { Skeleton } from '$lib/components/ui/skeleton';
|
||||||
|
import * as Card from '$lib/components/ui/card';
|
||||||
|
import { Badge } from '$lib/components/ui/badge';
|
||||||
|
import { Activity, ChartColumn, Maximize, Minimize } from 'lucide-svelte';
|
||||||
|
import { formatValue } from '$lib/utils';
|
||||||
|
import { allTradesStore } from '$lib/stores/websocket';
|
||||||
|
import { Button } from '$lib/components/ui/button';
|
||||||
|
|
||||||
|
interface CoinData {
|
||||||
|
symbol: string;
|
||||||
|
name: string;
|
||||||
|
currentPrice: number;
|
||||||
|
marketCap: number;
|
||||||
|
priceChange24h: number;
|
||||||
|
volume24h: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
let coins: CoinData[] = $state([]);
|
||||||
|
let isLoading = $state(true);
|
||||||
|
let error = $state<string | null>(null);
|
||||||
|
let lastUpdated = $state<Date>(new Date());
|
||||||
|
let isLiveUpdatesEnabled = $state(true);
|
||||||
|
let isFullscreen = $state(false);
|
||||||
|
let fullscreenContainer: HTMLDivElement;
|
||||||
|
|
||||||
|
let treemapOptions = $derived({
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
data: coins.map((coin) => {
|
||||||
|
const change = coin.priceChange24h;
|
||||||
|
|
||||||
|
if (Math.abs(change) < 0.5) {
|
||||||
|
return { x: coin.symbol, y: coin.marketCap, fillColor: 'rgba(107,114,128,0.3)' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const intensity = Math.min(Math.abs(change) / 100, 1);
|
||||||
|
const alpha = 0.3 + intensity * 0.7;
|
||||||
|
|
||||||
|
const base = change >= 0 ? '16,163,74' : '220,38,38';
|
||||||
|
return { x: coin.symbol, y: coin.marketCap, fillColor: `rgba(${base},${alpha})` };
|
||||||
|
})
|
||||||
|
}
|
||||||
|
],
|
||||||
|
chart: {
|
||||||
|
height: isFullscreen ? window.innerHeight - 300 : 600,
|
||||||
|
type: 'treemap',
|
||||||
|
toolbar: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
background: 'transparent',
|
||||||
|
animations: {
|
||||||
|
enabled: true,
|
||||||
|
easing: 'easeinout',
|
||||||
|
speed: 200
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dataLabels: {
|
||||||
|
enabled: true,
|
||||||
|
style: {
|
||||||
|
fontSize: '12px',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
colors: ['#ffffff']
|
||||||
|
},
|
||||||
|
formatter: function (text: string, op: any) {
|
||||||
|
const coin = coins.find((c) => c.symbol === text);
|
||||||
|
if (!coin) return [text];
|
||||||
|
const changeSign = coin.priceChange24h >= 0 ? '+' : '';
|
||||||
|
return [text, `${changeSign}${coin.priceChange24h.toFixed(2)}%`];
|
||||||
|
},
|
||||||
|
offsetY: -4
|
||||||
|
},
|
||||||
|
plotOptions: {
|
||||||
|
treemap: {
|
||||||
|
distributed: true,
|
||||||
|
enableShades: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
enabled: true,
|
||||||
|
custom: function ({ seriesIndex, dataPointIndex }: any) {
|
||||||
|
const coin = coins[dataPointIndex];
|
||||||
|
if (!coin) return '';
|
||||||
|
|
||||||
|
const changeColor = coin.priceChange24h >= 0 ? '#22c55e' : '#ef4444';
|
||||||
|
const changeSign = coin.priceChange24h >= 0 ? '+' : '';
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="p-3 bg-card border rounded-md shadow-lg">
|
||||||
|
<div class="font-semibold text-lg mb-2">*${coin.symbol}</div>
|
||||||
|
<div class="text-sm text-muted-foreground mb-1">${coin.name}</div>
|
||||||
|
<div class="space-y-1 text-xs">
|
||||||
|
<div>Price: <span class="font-mono">${formatValue(coin.currentPrice)}</span></div>
|
||||||
|
<div>Market Cap: <span class="font-mono">${formatValue(coin.marketCap)}</span></div>
|
||||||
|
<div>24h Volume: <span class="font-mono">${formatValue(coin.volume24h)}</span></div>
|
||||||
|
<div>24h Change: <span class="font-mono" style="color: ${changeColor}">${changeSign}${coin.priceChange24h.toFixed(2)}%</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
theme: {
|
||||||
|
mode: 'light'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if ($allTradesStore.length > 0 && isLiveUpdatesEnabled) {
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
fetchCoins();
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
|
return () => clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
function handleFullscreenChange() {
|
||||||
|
isFullscreen = !!document.fullscreenElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('fullscreenchange', handleFullscreenChange);
|
||||||
|
document.addEventListener('webkitfullscreenchange', handleFullscreenChange);
|
||||||
|
document.addEventListener('mozfullscreenchange', handleFullscreenChange);
|
||||||
|
document.addEventListener('MSFullscreenChange', handleFullscreenChange);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('fullscreenchange', handleFullscreenChange);
|
||||||
|
document.removeEventListener('webkitfullscreenchange', handleFullscreenChange);
|
||||||
|
document.removeEventListener('mozfullscreenchange', handleFullscreenChange);
|
||||||
|
document.removeEventListener('MSFullscreenChange', handleFullscreenChange);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
async function toggleFullscreen() {
|
||||||
|
if (!document.fullscreenElement) {
|
||||||
|
try {
|
||||||
|
await fullscreenContainer.requestFullscreen();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error attempting to enable fullscreen:', err);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
await document.exitFullscreen();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error attempting to exit fullscreen:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchCoins() {
|
||||||
|
try {
|
||||||
|
if (coins.length === 0) {
|
||||||
|
isLoading = true;
|
||||||
|
}
|
||||||
|
error = null;
|
||||||
|
|
||||||
|
const response = await fetch('/api/market?limit=100');
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch coins data');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
coins =
|
||||||
|
data.coins.map((coin: any) => ({
|
||||||
|
symbol: coin.symbol,
|
||||||
|
name: coin.name,
|
||||||
|
currentPrice: coin.currentPrice,
|
||||||
|
marketCap: coin.marketCap,
|
||||||
|
priceChange24h: coin.change24h,
|
||||||
|
volume24h: coin.volume24h
|
||||||
|
})) || [];
|
||||||
|
|
||||||
|
lastUpdated = new Date();
|
||||||
|
} catch (err) {
|
||||||
|
error = err instanceof Error ? err.message : 'An error occurred';
|
||||||
|
console.error('Error fetching coins:', err);
|
||||||
|
} finally {
|
||||||
|
isLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
fetchCoins();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>Treemap - Rugplay</title>
|
||||||
|
<meta name="description" content="Cryptocurrency market treemap visualization" />
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={fullscreenContainer}
|
||||||
|
class="treemap-container {isFullscreen ? 'fullscreen-mode' : ''}"
|
||||||
|
>
|
||||||
|
<div class="container mx-auto px-4 py-8 {isFullscreen ? 'fullscreen-content' : ''}">
|
||||||
|
<div class="mb-6">
|
||||||
|
<div class="mb-2 flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<ChartColumn class="h-6 w-6" />
|
||||||
|
<h1 class="text-2xl font-bold">Market Treemap</h1>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onclick={() => (isLiveUpdatesEnabled = !isLiveUpdatesEnabled)}
|
||||||
|
class={isLiveUpdatesEnabled
|
||||||
|
? 'border-green-500 text-green-500'
|
||||||
|
: 'border-red-500 text-red-500'}
|
||||||
|
>
|
||||||
|
{#if isLiveUpdatesEnabled}
|
||||||
|
<Activity class="h-4 w-4" />
|
||||||
|
Live
|
||||||
|
{:else}
|
||||||
|
<Activity class="h-4 w-4" />
|
||||||
|
Paused
|
||||||
|
{/if}
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" size="sm" onclick={toggleFullscreen}>
|
||||||
|
{#if isFullscreen}
|
||||||
|
<Minimize class="h-4 w-4" />
|
||||||
|
Exit Fullscreen
|
||||||
|
{:else}
|
||||||
|
<Maximize class="h-4 w-4" />
|
||||||
|
Fullscreen
|
||||||
|
{/if}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="text-muted-foreground">
|
||||||
|
Visual representation of the cryptocurrency market. Size indicates market cap, color shows
|
||||||
|
24h price change.
|
||||||
|
</p>
|
||||||
|
{#if coins.length > 0}
|
||||||
|
<p class="text-muted-foreground mt-1 text-sm">
|
||||||
|
Last updated: {lastUpdated.toLocaleTimeString()}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if isLoading && coins.length === 0}
|
||||||
|
<Card.Root>
|
||||||
|
<Card.Header>
|
||||||
|
<Card.Title class="flex items-center gap-2">
|
||||||
|
<Skeleton class="h-5 w-5" />
|
||||||
|
<Skeleton class="h-6 w-48" />
|
||||||
|
</Card.Title>
|
||||||
|
<Card.Description>
|
||||||
|
<Skeleton class="h-4 w-64" />
|
||||||
|
</Card.Description>
|
||||||
|
</Card.Header>
|
||||||
|
<Card.Content>
|
||||||
|
<Skeleton class="h-[600px] w-full" />
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
|
{:else if error}
|
||||||
|
<Card.Root>
|
||||||
|
<Card.Content class="p-8 text-center">
|
||||||
|
<div class="text-muted-foreground mb-4">
|
||||||
|
<ChartColumn class="mx-auto mb-2 h-12 w-12 opacity-50" />
|
||||||
|
<p class="text-lg font-medium">Failed to load treemap</p>
|
||||||
|
<p class="text-sm">{error}</p>
|
||||||
|
</div>
|
||||||
|
<Button onclick={fetchCoins}>Try Again</Button>
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
|
{:else if coins.length === 0}
|
||||||
|
<Card.Root>
|
||||||
|
<Card.Content class="p-8 text-center">
|
||||||
|
<div class="text-muted-foreground">
|
||||||
|
<ChartColumn class="mx-auto mb-2 h-12 w-12 opacity-50" />
|
||||||
|
<p class="text-lg font-medium">No coins available</p>
|
||||||
|
<p class="text-sm">Create some coins to see the treemap visualization.</p>
|
||||||
|
</div>
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
|
{:else}
|
||||||
|
<Card.Root>
|
||||||
|
<Card.Content class="p-6">
|
||||||
|
<div class="text-muted-foreground mb-4 flex flex-wrap gap-4 text-sm">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="h-3 w-3 rounded bg-green-500"></div>
|
||||||
|
<span>Positive 24h change</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="h-3 w-3 rounded bg-red-500"></div>
|
||||||
|
<span>Negative 24h change</span>
|
||||||
|
</div>
|
||||||
|
<Badge variant="outline" class="ml-auto">
|
||||||
|
{coins.length} coins
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div use:chart={treemapOptions}></div>
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.treemap-container.fullscreen-mode {
|
||||||
|
background: hsl(var(--background));
|
||||||
|
padding: 1rem;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.treemap-container.fullscreen-mode .container {
|
||||||
|
max-width: none;
|
||||||
|
padding: 0;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.treemap-container.fullscreen-mode .fullscreen-content {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.treemap-container.fullscreen-mode .mb-6 {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.treemap-container.fullscreen-mode :global(.card) {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.treemap-container.fullscreen-mode :global(.card .card-content) {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.fullscreen-mode) {
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import DataTable from '$lib/components/self/DataTable.svelte';
|
import DataTable from '$lib/components/self/DataTable.svelte';
|
||||||
import ProfileBadges from '$lib/components/self/ProfileBadges.svelte';
|
import ProfileBadges from '$lib/components/self/ProfileBadges.svelte';
|
||||||
|
import ProfileSkeleton from '$lib/components/self/skeletons/ProfileSkeleton.svelte';
|
||||||
import { getPublicUrl, formatPrice, formatValue, formatQuantity, formatDate } from '$lib/utils';
|
import { getPublicUrl, formatPrice, formatValue, formatQuantity, formatDate } from '$lib/utils';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
|
|
@ -15,8 +16,7 @@
|
||||||
TrendingDown,
|
TrendingDown,
|
||||||
Coins,
|
Coins,
|
||||||
Receipt,
|
Receipt,
|
||||||
Activity,
|
Activity
|
||||||
RefreshCw
|
|
||||||
} from 'lucide-svelte';
|
} from 'lucide-svelte';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import type { UserProfileData } from '$lib/types/user-profile';
|
import type { UserProfileData } from '$lib/types/user-profile';
|
||||||
|
|
@ -208,12 +208,7 @@
|
||||||
|
|
||||||
<div class="container mx-auto max-w-6xl p-6">
|
<div class="container mx-auto max-w-6xl p-6">
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<div class="flex h-96 items-center justify-center">
|
<ProfileSkeleton />
|
||||||
<div class="text-center">
|
|
||||||
<RefreshCw class="text-muted-foreground mx-auto mb-4 h-8 w-8 animate-spin" />
|
|
||||||
<div class="text-xl">Loading profile...</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{:else if !profileData}
|
{:else if !profileData}
|
||||||
<div class="flex h-96 items-center justify-center">
|
<div class="flex h-96 items-center justify-center">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
|
|
|
||||||
Reference in a new issue