This commit is contained in:
Face 2025-05-21 21:34:22 +03:00
parent 3b2ec4fe5f
commit 8086aa8f38
51 changed files with 4109 additions and 0 deletions

112
website/src/app.css Normal file
View file

@ -0,0 +1,112 @@
@import "tailwindcss";
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.129 0.042 264.695);
--card: oklch(1 0 0);
--card-foreground: oklch(0.129 0.042 264.695);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.129 0.042 264.695);
--primary: oklch(0.208 0.042 265.755);
--primary-foreground: oklch(0.984 0.003 247.858);
--secondary: oklch(0.968 0.007 247.896);
--secondary-foreground: oklch(0.208 0.042 265.755);
--muted: oklch(0.968 0.007 247.896);
--muted-foreground: oklch(0.554 0.046 257.417);
--accent: oklch(0.968 0.007 247.896);
--accent-foreground: oklch(0.208 0.042 265.755);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.929 0.013 255.508);
--input: oklch(0.929 0.013 255.508);
--ring: oklch(0.704 0.04 256.788);
--sidebar: oklch(0.984 0.003 247.858);
--sidebar-foreground: oklch(0.129 0.042 264.695);
--sidebar-primary: oklch(0.208 0.042 265.755);
--sidebar-primary-foreground: oklch(0.984 0.003 247.858);
--sidebar-accent: oklch(0.968 0.007 247.896);
--sidebar-accent-foreground: oklch(0.208 0.042 265.755);
--sidebar-border: oklch(0.929 0.013 255.508);
--sidebar-ring: oklch(0.704 0.04 256.788);
}
.dark {
--background: oklch(0.129 0.042 264.695);
--foreground: oklch(0.984 0.003 247.858);
--card: oklch(0.208 0.042 265.755);
--card-foreground: oklch(0.984 0.003 247.858);
--popover: oklch(0.208 0.042 265.755);
--popover-foreground: oklch(0.984 0.003 247.858);
--primary: oklch(0.929 0.013 255.508);
--primary-foreground: oklch(0.208 0.042 265.755);
--secondary: oklch(0.279 0.041 260.031);
--secondary-foreground: oklch(0.984 0.003 247.858);
--muted: oklch(0.279 0.041 260.031);
--muted-foreground: oklch(0.704 0.04 256.788);
--accent: oklch(0.279 0.041 260.031);
--accent-foreground: oklch(0.984 0.003 247.858);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.551 0.027 264.364);
--sidebar: oklch(0.208 0.042 265.755);
--sidebar-foreground: oklch(0.984 0.003 247.858);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.984 0.003 247.858);
--sidebar-accent: oklch(0.279 0.041 260.031);
--sidebar-accent-foreground: oklch(0.984 0.003 247.858);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.551 0.027 264.364);
}
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}

13
website/src/app.d.ts vendored Normal file
View file

@ -0,0 +1,13 @@
// See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};

12
website/src/app.html Normal file
View file

@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

View file

@ -0,0 +1,52 @@
<script lang="ts" module>
import { type VariantProps, tv } from "tailwind-variants";
export const badgeVariants = tv({
base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden whitespace-nowrap rounded-md border px-2 py-0.5 text-xs font-medium transition-[color,box-shadow] focus-visible:ring-[3px] [&>svg]:pointer-events-none [&>svg]:size-3",
variants: {
variant: {
default:
"bg-primary text-primary-foreground [a&]:hover:bg-primary/90 border-transparent",
secondary:
"bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90 border-transparent",
destructive:
"bg-destructive [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/70 border-transparent text-white",
outline: "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
success:
"bg-green-600 hover:bg-green-700 border-transparent text-white",
},
},
defaultVariants: {
variant: "default",
},
});
export type BadgeVariant = VariantProps<typeof badgeVariants>["variant"];
</script>
<script lang="ts">
import type { HTMLAnchorAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
href,
class: className,
variant = "default",
children,
...restProps
}: WithElementRef<HTMLAnchorAttributes> & {
variant?: BadgeVariant;
} = $props();
</script>
<svelte:element
this={href ? "a" : "span"}
bind:this={ref}
data-slot="badge"
{href}
class={cn(badgeVariants({ variant }), className)}
{...restProps}
>
{@render children?.()}
</svelte:element>

View file

@ -0,0 +1,2 @@
export { default as Badge } from "./badge.svelte";
export { badgeVariants, type BadgeVariant } from "./badge.svelte";

View file

@ -0,0 +1,80 @@
<script lang="ts" module>
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements";
import { type VariantProps, tv } from "tailwind-variants";
export const buttonVariants = tv({
base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium outline-none transition-all focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
variants: {
variant: {
default: "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
destructive:
"bg-destructive shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 text-white",
outline:
"bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 border",
secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
});
export type ButtonVariant = VariantProps<typeof buttonVariants>["variant"];
export type ButtonSize = VariantProps<typeof buttonVariants>["size"];
export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
WithElementRef<HTMLAnchorAttributes> & {
variant?: ButtonVariant;
size?: ButtonSize;
};
</script>
<script lang="ts">
let {
class: className,
variant = "default",
size = "default",
ref = $bindable(null),
href = undefined,
type = "button",
disabled,
children,
...restProps
}: ButtonProps = $props();
</script>
{#if href}
<a
bind:this={ref}
data-slot="button"
class={cn(buttonVariants({ variant, size }), className)}
href={disabled ? undefined : href}
aria-disabled={disabled}
role={disabled ? "link" : undefined}
tabindex={disabled ? -1 : undefined}
{...restProps}
>
{@render children?.()}
</a>
{:else}
<button
bind:this={ref}
data-slot="button"
class={cn(buttonVariants({ variant, size }), className)}
{type}
{disabled}
{...restProps}
>
{@render children?.()}
</button>
{/if}

View file

@ -0,0 +1,17 @@
import Root, {
type ButtonProps,
type ButtonSize,
type ButtonVariant,
buttonVariants,
} from "./button.svelte";
export {
Root,
type ButtonProps as Props,
//
Root as Button,
buttonVariants,
type ButtonProps,
type ButtonSize,
type ButtonVariant,
};

View file

@ -0,0 +1,20 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="card-action"
class={cn("col-start-2 row-span-2 row-start-1 self-start justify-self-end", className)}
{...restProps}
>
{@render children?.()}
</div>

View file

@ -0,0 +1,15 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div bind:this={ref} data-slot="card-content" class={cn("px-6", className)} {...restProps}>
{@render children?.()}
</div>

View file

@ -0,0 +1,20 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLParagraphElement>> = $props();
</script>
<p
bind:this={ref}
data-slot="card-description"
class={cn("text-muted-foreground text-sm", className)}
{...restProps}
>
{@render children?.()}
</p>

View file

@ -0,0 +1,20 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="card-footer"
class={cn("[.border-t]:pt-6 flex items-center px-6", className)}
{...restProps}
>
{@render children?.()}
</div>

View file

@ -0,0 +1,23 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="card-header"
class={cn(
"@container/card-header has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6 grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6",
className
)}
{...restProps}
>
{@render children?.()}
</div>

View file

@ -0,0 +1,20 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="card-title"
class={cn("font-semibold leading-none", className)}
{...restProps}
>
{@render children?.()}
</div>

View file

@ -0,0 +1,23 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="card"
class={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
className
)}
{...restProps}
>
{@render children?.()}
</div>

View file

@ -0,0 +1,25 @@
import Root from "./card.svelte";
import Content from "./card-content.svelte";
import Description from "./card-description.svelte";
import Footer from "./card-footer.svelte";
import Header from "./card-header.svelte";
import Title from "./card-title.svelte";
import Action from "./card-action.svelte";
export {
Root,
Content,
Description,
Footer,
Header,
Title,
Action,
//
Root as Card,
Content as CardContent,
Description as CardDescription,
Footer as CardFooter,
Header as CardHeader,
Title as CardTitle,
Action as CardAction,
};

View file

@ -0,0 +1,28 @@
import Root from "./table.svelte";
import Body from "./table-body.svelte";
import Caption from "./table-caption.svelte";
import Cell from "./table-cell.svelte";
import Footer from "./table-footer.svelte";
import Head from "./table-head.svelte";
import Header from "./table-header.svelte";
import Row from "./table-row.svelte";
export {
Root,
Body,
Caption,
Cell,
Footer,
Head,
Header,
Row,
//
Root as Table,
Body as TableBody,
Caption as TableCaption,
Cell as TableCell,
Footer as TableFooter,
Head as TableHead,
Header as TableHeader,
Row as TableRow,
};

View file

@ -0,0 +1,20 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLTableSectionElement>> = $props();
</script>
<tbody
bind:this={ref}
data-slot="table-body"
class={cn("[&_tr:last-child]:border-0", className)}
{...restProps}
>
{@render children?.()}
</tbody>

View file

@ -0,0 +1,20 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
</script>
<caption
bind:this={ref}
data-slot="table-caption"
class={cn("text-muted-foreground mt-4 text-sm", className)}
{...restProps}
>
{@render children?.()}
</caption>

View file

@ -0,0 +1,20 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLTdAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLTdAttributes> = $props();
</script>
<td
bind:this={ref}
data-slot="table-cell"
class={cn("whitespace-nowrap p-2 align-middle [&:has([role=checkbox])]:pr-0", className)}
{...restProps}
>
{@render children?.()}
</td>

View file

@ -0,0 +1,20 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLTableSectionElement>> = $props();
</script>
<tfoot
bind:this={ref}
data-slot="table-footer"
class={cn("bg-muted/50 border-t font-medium [&>tr]:last:border-b-0", className)}
{...restProps}
>
{@render children?.()}
</tfoot>

View file

@ -0,0 +1,23 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLThAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLThAttributes> = $props();
</script>
<th
bind:this={ref}
data-slot="table-head"
class={cn(
"text-foreground h-10 whitespace-nowrap px-2 text-left align-middle font-medium [&:has([role=checkbox])]:pr-0",
className
)}
{...restProps}
>
{@render children?.()}
</th>

View file

@ -0,0 +1,20 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLTableSectionElement>> = $props();
</script>
<thead
bind:this={ref}
data-slot="table-header"
class={cn("[&_tr]:border-b", className)}
{...restProps}
>
{@render children?.()}
</thead>

View file

@ -0,0 +1,23 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLTableRowElement>> = $props();
</script>
<tr
bind:this={ref}
data-slot="table-row"
class={cn(
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
className
)}
{...restProps}
>
{@render children?.()}
</tr>

View file

@ -0,0 +1,22 @@
<script lang="ts">
import type { HTMLTableAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLTableAttributes> = $props();
</script>
<div data-slot="table-container" class="relative w-full overflow-x-auto">
<table
bind:this={ref}
data-slot="table"
class={cn("w-full caption-bottom text-sm", className)}
{...restProps}
>
{@render children?.()}
</table>
</div>

View file

@ -0,0 +1,103 @@
export interface Coin {
id: number;
name: string;
symbol: string;
price: number;
change24h: number;
volume24h: number;
marketCap: number;
priceHistory: { date: string; price: number }[];
}
export const coins: Coin[] = [
{
id: 1,
name: 'Bitcoin',
symbol: 'BTC',
price: 67890.42,
change24h: 2.3,
volume24h: 28500000000,
marketCap: 1320000000000,
priceHistory: [
{ date: '2025-05-14', price: 66250.18 },
{ date: '2025-05-15', price: 65890.34 },
{ date: '2025-05-16', price: 66780.12 },
{ date: '2025-05-17', price: 66920.45 },
{ date: '2025-05-18', price: 67120.78 },
{ date: '2025-05-19', price: 67450.23 },
{ date: '2025-05-20', price: 67890.42 }
]
},
{
id: 2,
name: 'Ethereum',
symbol: 'ETH',
price: 3456.78,
change24h: -1.2,
volume24h: 15200000000,
marketCap: 420000000000,
priceHistory: [
{ date: '2025-05-14', price: 3520.45 },
{ date: '2025-05-15', price: 3490.23 },
{ date: '2025-05-16', price: 3475.67 },
{ date: '2025-05-17', price: 3460.12 },
{ date: '2025-05-18', price: 3470.54 },
{ date: '2025-05-19', price: 3465.89 },
{ date: '2025-05-20', price: 3456.78 }
]
},
{
id: 3,
name: 'Ripple',
symbol: 'XRP',
price: 0.54,
change24h: 5.7,
volume24h: 2100000000,
marketCap: 28500000000,
priceHistory: [
{ date: '2025-05-14', price: 0.49 },
{ date: '2025-05-15', price: 0.50 },
{ date: '2025-05-16', price: 0.51 },
{ date: '2025-05-17', price: 0.52 },
{ date: '2025-05-18', price: 0.53 },
{ date: '2025-05-19', price: 0.54 },
{ date: '2025-05-20', price: 0.54 }
]
},
{
id: 4,
name: 'Solana',
symbol: 'SOL',
price: 156.89,
change24h: 7.2,
volume24h: 5600000000,
marketCap: 67800000000,
priceHistory: [
{ date: '2025-05-14', price: 142.34 },
{ date: '2025-05-15', price: 145.67 },
{ date: '2025-05-16', price: 148.90 },
{ date: '2025-05-17', price: 150.25 },
{ date: '2025-05-18', price: 152.30 },
{ date: '2025-05-19', price: 154.75 },
{ date: '2025-05-20', price: 156.89 }
]
},
{
id: 5,
name: 'Dogecoin',
symbol: 'DOGE',
price: 0.12,
change24h: -2.5,
volume24h: 980000000,
marketCap: 16500000000,
priceHistory: [
{ date: '2025-05-14', price: 0.125 },
{ date: '2025-05-15', price: 0.124 },
{ date: '2025-05-16', price: 0.123 },
{ date: '2025-05-17', price: 0.122 },
{ date: '2025-05-18', price: 0.121 },
{ date: '2025-05-19', price: 0.120 },
{ date: '2025-05-20', price: 0.120 }
]
}
];

1
website/src/lib/index.ts Normal file
View file

@ -0,0 +1 @@
// place files you want to import through the `$lib` alias in this folder.

View file

@ -0,0 +1,6 @@
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import { env } from '$env/dynamic/private';
if (!env.DATABASE_URL) throw new Error('DATABASE_URL is not set');
const client = postgres(env.DATABASE_URL);
export const db = drizzle(client);

View file

@ -0,0 +1,6 @@
import { pgTable, serial, text, integer } from 'drizzle-orm/pg-core';
export const user = pgTable('user', {
id: serial('id').primaryKey(),
age: integer('age')
});

13
website/src/lib/utils.ts Normal file
View file

@ -0,0 +1,13 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type WithoutChild<T> = T extends { child?: any } ? Omit<T, "child"> : T;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type WithoutChildren<T> = T extends { children?: any } ? Omit<T, "children"> : T;
export type WithoutChildrenOrChild<T> = WithoutChildren<WithoutChild<T>>;
export type WithElementRef<T, U extends HTMLElement = HTMLElement> = T & { ref?: U | null };

View file

@ -0,0 +1,6 @@
<script lang="ts">
import '../app.css';
let { children } = $props();
</script>
{@render children()}

View file

@ -0,0 +1 @@
export const ssr = false;

View file

@ -0,0 +1,93 @@
<script lang="ts">
import { coins } from '$lib/data/coins';
import * as Card from '$lib/components/ui/card';
import * as Table from '$lib/components/ui/table';
import { Badge } from '$lib/components/ui/badge';
</script>
<div class="container mx-auto p-6">
<header class="mb-8">
<h1 class="mb-2 text-4xl font-bold">Rugplay</h1>
<p class="text-muted-foreground">A trading simulator</p>
</header>
<div class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
{#each coins as coin}
<a href={`/coin/${coin.symbol}`} class="block">
<Card.Root class="h-full transition-shadow hover:shadow-md">
<Card.Header>
<Card.Title class="flex items-center justify-between">
<span>{coin.name} ({coin.symbol})</span>
<Badge variant={coin.change24h >= 0 ? 'success' : 'destructive'} class="ml-2">
{coin.change24h >= 0 ? '+' : ''}{coin.change24h}%
</Badge>
</Card.Title>
<Card.Description
>Market Cap: ${(coin.marketCap / 1000000000).toFixed(2)}B</Card.Description
>
</Card.Header>
<Card.Content>
<div class="flex items-baseline justify-between">
<span class="text-3xl font-bold"
>${coin.price.toLocaleString(undefined, {
minimumFractionDigits: coin.price < 1 ? 3 : 2,
maximumFractionDigits: coin.price < 1 ? 3 : 2
})}</span
>
<span class="text-muted-foreground text-sm"
>24h Vol: ${(coin.volume24h / 1000000000).toFixed(2)}B</span
>
</div>
</Card.Content>
</Card.Root>
</a>
{/each}
</div>
<div class="mt-12">
<h2 class="mb-4 text-2xl font-bold">Market Overview</h2>
<Card.Root>
<Card.Content class="p-0">
<Table.Root>
<Table.Header>
<Table.Row>
<Table.Head>Name</Table.Head>
<Table.Head>Price</Table.Head>
<Table.Head>24h Change</Table.Head>
<Table.Head class="hidden md:table-cell">Market Cap</Table.Head>
<Table.Head class="hidden md:table-cell">Volume (24h)</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#each coins as coin}
<Table.Row>
<Table.Cell class="font-medium">
<a href={`/coin/${coin.symbol}`} class="hover:underline">
{coin.name} <span class="text-muted-foreground">({coin.symbol})</span>
</a>
</Table.Cell>
<Table.Cell
>${coin.price.toLocaleString(undefined, {
minimumFractionDigits: coin.price < 1 ? 3 : 2,
maximumFractionDigits: coin.price < 1 ? 3 : 2
})}</Table.Cell
>
<Table.Cell>
<Badge variant={coin.change24h >= 0 ? 'success' : 'destructive'}>
{coin.change24h >= 0 ? '+' : ''}{coin.change24h}%
</Badge>
</Table.Cell>
<Table.Cell class="hidden md:table-cell"
>${(coin.marketCap / 1000000000).toFixed(2)}B</Table.Cell
>
<Table.Cell class="hidden md:table-cell"
>${(coin.volume24h / 1000000000).toFixed(2)}B</Table.Cell
>
</Table.Row>
{/each}
</Table.Body>
</Table.Root>
</Card.Content>
</Card.Root>
</div>
</div>

View file

@ -0,0 +1,144 @@
<script lang="ts">
import { page } from '$app/stores';
import { coins } from '$lib/data/coins';
import * as Card from '$lib/components/ui/card';
import { Badge } from '$lib/components/ui/badge';
import { createChart, CandlestickSeries, type Time, ColorType } from 'lightweight-charts';
import { onMount } from 'svelte';
const coin = coins.find((c) => c.symbol === $page.params.coinSymbol);
// Generate mock candlestick data
const candleData = Array.from({ length: 30 }, (_, i) => {
const basePrice = coin?.price || 100;
const date = new Date(Date.now() - (29 - i) * 24 * 60 * 60 * 1000);
const open = basePrice * (1 + Math.sin(i / 5) * 0.1);
const close = basePrice * (1 + Math.sin((i + 1) / 5) * 0.1);
const high = Math.max(open, close) * (1 + Math.random() * 0.02);
const low = Math.min(open, close) * (1 - Math.random() * 0.02);
return {
time: Math.floor(date.getTime() / 1000) as Time,
open,
high,
low,
close
};
});
let chartContainer: HTMLDivElement;
onMount(() => {
const chart = createChart(chartContainer, {
layout: {
textColor: '#666666',
background: { type: ColorType.Solid, color: 'transparent' },
attributionLogo: false
},
grid: {
vertLines: { color: '#2B2B43' },
horzLines: { color: '#2B2B43' }
},
rightPriceScale: {
borderVisible: false
},
timeScale: {
borderVisible: false,
timeVisible: true
},
crosshair: {
mode: 1
}
});
const candlesticks = chart.addSeries(CandlestickSeries, {
upColor: '#26a69a',
downColor: '#ef5350',
borderVisible: false,
wickUpColor: '#26a69a',
wickDownColor: '#ef5350'
});
candlesticks.setData(candleData);
chart.timeScale().fitContent();
const handleResize = () => {
chart.applyOptions({
width: chartContainer.clientWidth
});
};
window.addEventListener('resize', handleResize);
handleResize();
return () => {
window.removeEventListener('resize', handleResize);
chart.remove();
};
});
</script>
<div class="container mx-auto p-6">
{#if coin}
<header class="mb-8">
<div class="flex items-center justify-between">
<h1 class="text-4xl font-bold">{coin.name} ({coin.symbol})</h1>
<Badge variant={coin.change24h >= 0 ? 'success' : 'destructive'} class="text-lg">
{coin.change24h >= 0 ? '+' : ''}{coin.change24h}%
</Badge>
</div>
<p class="mt-4 text-3xl font-semibold">
${coin.price.toLocaleString(undefined, {
minimumFractionDigits: coin.price < 1 ? 3 : 2,
maximumFractionDigits: coin.price < 1 ? 3 : 2
})}
</p>
</header>
<div class="grid gap-6">
<Card.Root>
<Card.Header>
<Card.Title>Price Chart</Card.Title>
</Card.Header>
<Card.Content>
<div class="h-[400px] w-full" bind:this={chartContainer}></div>
</Card.Content>
</Card.Root>
<div class="grid grid-cols-1 gap-6 md:grid-cols-3">
<Card.Root>
<Card.Header>
<Card.Title>Market Cap</Card.Title>
</Card.Header>
<Card.Content>
<p class="text-2xl font-semibold">${(coin.marketCap / 1000000000).toFixed(2)}B</p>
</Card.Content>
</Card.Root>
<Card.Root>
<Card.Header>
<Card.Title>24h Volume</Card.Title>
</Card.Header>
<Card.Content>
<p class="text-2xl font-semibold">${(coin.volume24h / 1000000000).toFixed(2)}B</p>
</Card.Content>
</Card.Root>
<Card.Root>
<Card.Header>
<Card.Title>24h Change</Card.Title>
</Card.Header>
<Card.Content>
<p class="text-2xl font-semibold">
<Badge variant={coin.change24h >= 0 ? 'success' : 'destructive'} class="text-lg">
{coin.change24h >= 0 ? '+' : ''}{coin.change24h}%
</Badge>
</p>
</Card.Content>
</Card.Root>
</div>
</div>
{:else}
<p>Coin not found</p>
{/if}
</div>