add swap card + bybit webhook

master
TZGyn 12 months ago
parent 0f4630a534
commit b61f557380
Signed by: TZGyn
GPG Key ID: 122EAF77AE81FD4A

2894
bun.lock

File diff suppressed because it is too large Load Diff

Binary file not shown.

@ -0,0 +1,11 @@
// drizzle.config.ts
import { defineConfig } from 'drizzle-kit'
export default defineConfig({
schema: './src/lib/db/schema.ts',
out: './drizzle',
dialect: 'postgresql',
dbCredentials: {
url: process.env.DATABASE_URL as string,
},
})

@ -9,34 +9,48 @@
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"format": "prettier --write .",
"lint": "prettier --check ."
"lint": "prettier --check .",
"db:push": "drizzle-kit push",
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate"
},
"devDependencies": {
"@internationalized/date": "^3.5.6",
"@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/kit": "^2.0.0",
"@sveltejs/kit": "^2.17.1",
"@sveltejs/vite-plugin-svelte": "^4.0.0",
"@types/bun": "^1.2.2",
"autoprefixer": "^10.4.20",
"bits-ui": "^1.0.0-next.64",
"bits-ui": "^1.0.0-next.86",
"clsx": "^2.1.1",
"drizzle-kit": "^0.30.4",
"formsnap": "^2.0.0-next.1",
"lucide-svelte": "^0.454.0",
"lucide-svelte": "^0.471.0",
"prettier": "^3.3.2",
"prettier-plugin-svelte": "^3.2.6",
"prettier-plugin-tailwindcss": "^0.6.5",
"svelte": "^5.0.0",
"prettier-plugin-svelte": "^3.3.2",
"prettier-plugin-tailwindcss": "^0.6.9",
"svelte": "^5.19.8",
"svelte-check": "^4.0.0",
"sveltekit-superforms": "^2.20.0",
"svelte-sonner": "^0.3.28",
"sveltekit-superforms": "^2.23.1",
"tailwind-merge": "^2.5.4",
"tailwind-variants": "^0.2.1",
"tailwindcss": "^3.4.9",
"tailwindcss-animate": "^1.0.7",
"typescript": "^5.0.0",
"vite": "^5.0.3",
"zod": "^3.23.8"
"zod": "^3.24.1"
},
"dependencies": {
"@node-rs/argon2": "^2.0.2",
"@oslojs/crypto": "^1.0.1",
"@oslojs/encoding": "^1.1.0",
"@sifi/sdk": "^1.12.0",
"axios": "^1.7.7",
"mode-watcher": "^0.4.1"
"bybit-api": "^3.10.31",
"drizzle-orm": "^0.39.2",
"mode-watcher": "^0.5.0",
"pg": "^8.13.1",
"rubic-sdk": "^5.49.11"
}
}

15
src/app.d.ts vendored

@ -1,13 +1,24 @@
// See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces
import { user } from '$lib/db/schema'
import type { InferSelectModel } from 'drizzle-orm'
declare global {
namespace App {
// interface Error {}
// interface Locals {}
interface Locals {
user: Omit<InferSelectModel<typeof user>, 'password'>
session: {
id: string
userId: string
expiresAt: number
} | null
}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};
export {}

@ -0,0 +1,37 @@
import { json, type Handle } from '@sveltejs/kit'
import * as auth from '$lib/server/auth.js'
const handleAuth: Handle = async ({ event, resolve }) => {
const pathname = event.url.pathname
if (pathname.startsWith('/api')) {
const token = event.cookies.get('auth-session') ?? null
if (token === null) {
return json({}, { status: 401 })
}
const { session, user } = await auth.validateSessionToken(token)
if (!user) {
return json({}, { status: 401 })
}
if (session !== null) {
auth.setSessionTokenCookie(
event,
token,
new Date(session.expiresAt),
)
} else {
auth.deleteSessionTokenCookie(event)
}
event.locals.session = session
event.locals.user = user
}
return resolve(event)
}
export const handle = handleAuth

@ -0,0 +1,12 @@
import { goto } from '$app/navigation'
export const customFetch = async (
url: string,
props?: RequestInit,
) => {
const response = await fetch(url, { ...props })
if (response.status === 401) goto('/login')
return response
}

@ -0,0 +1,16 @@
<script lang="ts">
import { Avatar as AvatarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: AvatarPrimitive.FallbackProps = $props();
</script>
<AvatarPrimitive.Fallback
bind:ref
class={cn("bg-muted flex h-full w-full items-center justify-center rounded-full", className)}
{...restProps}
/>

@ -0,0 +1,16 @@
<script lang="ts">
import { Avatar as AvatarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: AvatarPrimitive.ImageProps = $props();
</script>
<AvatarPrimitive.Image
bind:ref
class={cn("aspect-square h-full w-full", className)}
{...restProps}
/>

@ -0,0 +1,16 @@
<script lang="ts">
import { Avatar as AvatarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: AvatarPrimitive.RootProps = $props();
</script>
<AvatarPrimitive.Root
bind:ref
class={cn("relative flex size-10 shrink-0 overflow-hidden rounded-full", className)}
{...restProps}
/>

@ -0,0 +1,13 @@
import Root from "./avatar.svelte";
import Image from "./avatar-image.svelte";
import Fallback from "./avatar-fallback.svelte";
export {
Root,
Image,
Fallback,
//
Root as Avatar,
Image as AvatarImage,
Fallback as AvatarFallback,
};

@ -56,7 +56,7 @@
{#if href}
<a
bind:this={ref}
class={cn(buttonVariants({ variant, size, className }))}
class={cn(buttonVariants({ variant, size }), className)}
{href}
{...restProps}
>
@ -65,7 +65,7 @@
{:else}
<button
bind:this={ref}
class={cn(buttonVariants({ variant, size, className }))}
class={cn(buttonVariants({ variant, size }), className)}
{type}
{...restProps}
>

@ -0,0 +1,35 @@
<script lang="ts">
import type {
Command as CommandPrimitive,
Dialog as DialogPrimitive,
WithoutChildrenOrChild,
} from "bits-ui";
import type { Snippet } from "svelte";
import Command from "./command.svelte";
import * as Dialog from "$lib/components/ui/dialog/index.js";
let {
open = $bindable(false),
ref = $bindable(null),
value = $bindable(""),
portalProps,
children,
...restProps
}: WithoutChildrenOrChild<DialogPrimitive.RootProps> &
WithoutChildrenOrChild<CommandPrimitive.RootProps> & {
portalProps?: DialogPrimitive.PortalProps;
children: Snippet;
} = $props();
</script>
<Dialog.Root bind:open {...restProps}>
<Dialog.Content class="overflow-hidden p-0 shadow-lg" {portalProps}>
<Command
class="[&_[data-command-group]:not([hidden])_~[data-command-group]]:pt-0 [&_[data-command-group]]:px-2 [&_[data-command-input-wrapper]_svg]:h-5 [&_[data-command-input-wrapper]_svg]:w-5 [&_[data-command-input]]:h-12 [&_[data-command-item]]:px-2 [&_[data-command-item]]:py-3 [&_[data-command-item]_svg]:h-5 [&_[data-command-item]_svg]:w-5"
{...restProps}
bind:value
bind:ref
{children}
/>
</Dialog.Content>
</Dialog.Root>

@ -0,0 +1,12 @@
<script lang="ts">
import { Command as CommandPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: CommandPrimitive.EmptyProps = $props();
</script>
<CommandPrimitive.Empty class={cn("py-6 text-center text-sm", className)} {...restProps} />

@ -0,0 +1,29 @@
<script lang="ts">
import { Command as CommandPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
heading,
...restProps
}: CommandPrimitive.GroupProps & {
heading?: string;
} = $props();
</script>
<CommandPrimitive.Group
class={cn("text-foreground overflow-hidden p-1", className)}
bind:ref
{...restProps}
>
{#if heading}
<CommandPrimitive.GroupHeading
class="text-muted-foreground px-2 py-1.5 text-xs font-medium"
>
{heading}
</CommandPrimitive.GroupHeading>
{/if}
<CommandPrimitive.GroupItems {children} />
</CommandPrimitive.Group>

@ -0,0 +1,25 @@
<script lang="ts">
import { Command as CommandPrimitive } from "bits-ui";
import Search from "lucide-svelte/icons/search";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
value = $bindable(""),
...restProps
}: CommandPrimitive.InputProps = $props();
</script>
<div class="flex items-center border-b px-2" data-command-input-wrapper="">
<Search class="mr-2 size-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
class={cn(
"placeholder:text-muted-foreground flex h-11 w-full rounded-md bg-transparent py-3 text-base outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
bind:ref
{...restProps}
bind:value
/>
</div>

@ -0,0 +1,19 @@
<script lang="ts">
import { Command as CommandPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: CommandPrimitive.ItemProps = $props();
</script>
<CommandPrimitive.Item
class={cn(
"aria-selected:bg-accent aria-selected:text-accent-foreground relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
className
)}
bind:ref
{...restProps}
/>

@ -0,0 +1,19 @@
<script lang="ts">
import { Command as CommandPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: CommandPrimitive.LinkItemProps = $props();
</script>
<CommandPrimitive.LinkItem
class={cn(
"aria-selected:bg-accent aria-selected:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
bind:ref
{...restProps}
/>

@ -0,0 +1,16 @@
<script lang="ts">
import { Command as CommandPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: CommandPrimitive.ListProps = $props();
</script>
<CommandPrimitive.List
class={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
{...restProps}
bind:ref
/>

@ -0,0 +1,12 @@
<script lang="ts">
import { Command as CommandPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: CommandPrimitive.SeparatorProps = $props();
</script>
<CommandPrimitive.Separator class={cn("bg-border -mx-1 h-px", className)} bind:ref {...restProps} />

@ -0,0 +1,20 @@
<script lang="ts">
import type { WithElementRef } from "bits-ui";
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLSpanElement>> = $props();
</script>
<span
bind:this={ref}
class={cn("text-muted-foreground ml-auto text-xs tracking-widest", className)}
{...restProps}
>
{@render children?.()}
</span>

@ -0,0 +1,21 @@
<script lang="ts">
import { Command as CommandPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
value = $bindable(""),
class: className,
...restProps
}: CommandPrimitive.RootProps = $props();
</script>
<CommandPrimitive.Root
class={cn(
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
className
)}
bind:value
bind:ref
{...restProps}
/>

@ -0,0 +1,40 @@
import { Command as CommandPrimitive } from "bits-ui";
import Root from "./command.svelte";
import Dialog from "./command-dialog.svelte";
import Empty from "./command-empty.svelte";
import Group from "./command-group.svelte";
import Item from "./command-item.svelte";
import Input from "./command-input.svelte";
import List from "./command-list.svelte";
import Separator from "./command-separator.svelte";
import Shortcut from "./command-shortcut.svelte";
import LinkItem from "./command-link-item.svelte";
const Loading = CommandPrimitive.Loading;
export {
Root,
Dialog,
Empty,
Group,
Item,
LinkItem,
Input,
List,
Separator,
Shortcut,
Loading,
//
Root as Command,
Dialog as CommandDialog,
Empty as CommandEmpty,
Group as CommandGroup,
Item as CommandItem,
LinkItem as CommandLinkItem,
Input as CommandInput,
List as CommandList,
Separator as CommandSeparator,
Shortcut as CommandShortcut,
Loading as CommandLoading,
};

@ -0,0 +1,38 @@
<script lang="ts">
import { Dialog as DialogPrimitive, type WithoutChildrenOrChild } from "bits-ui";
import X from "lucide-svelte/icons/x";
import type { Snippet } from "svelte";
import * as Dialog from "./index.js";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
portalProps,
children,
...restProps
}: WithoutChildrenOrChild<DialogPrimitive.ContentProps> & {
portalProps?: DialogPrimitive.PortalProps;
children: Snippet;
} = $props();
</script>
<Dialog.Portal {...portalProps}>
<Dialog.Overlay />
<DialogPrimitive.Content
bind:ref
class={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] bg-background fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg duration-200 sm:rounded-lg",
className
)}
{...restProps}
>
{@render children?.()}
<DialogPrimitive.Close
class="ring-offset-background focus:ring-ring absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none"
>
<X class="size-4" />
<span class="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</Dialog.Portal>

@ -0,0 +1,16 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: DialogPrimitive.DescriptionProps = $props();
</script>
<DialogPrimitive.Description
bind:ref
class={cn("text-muted-foreground text-sm", className)}
{...restProps}
/>

@ -0,0 +1,20 @@
<script lang="ts">
import type { WithElementRef } from "bits-ui";
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
class={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
{...restProps}
>
{@render children?.()}
</div>

@ -0,0 +1,20 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import type { WithElementRef } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
class={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)}
{...restProps}
>
{@render children?.()}
</div>

@ -0,0 +1,19 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: DialogPrimitive.OverlayProps = $props();
</script>
<DialogPrimitive.Overlay
bind:ref
class={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80",
className
)}
{...restProps}
/>

@ -0,0 +1,16 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: DialogPrimitive.TitleProps = $props();
</script>
<DialogPrimitive.Title
bind:ref
class={cn("text-lg font-semibold leading-none tracking-tight", className)}
{...restProps}
/>

@ -0,0 +1,37 @@
import { Dialog as DialogPrimitive } from "bits-ui";
import Title from "./dialog-title.svelte";
import Footer from "./dialog-footer.svelte";
import Header from "./dialog-header.svelte";
import Overlay from "./dialog-overlay.svelte";
import Content from "./dialog-content.svelte";
import Description from "./dialog-description.svelte";
const Root = DialogPrimitive.Root;
const Trigger = DialogPrimitive.Trigger;
const Close = DialogPrimitive.Close;
const Portal = DialogPrimitive.Portal;
export {
Root,
Title,
Portal,
Footer,
Header,
Trigger,
Overlay,
Content,
Description,
Close,
//
Root as Dialog,
Title as DialogTitle,
Portal as DialogPortal,
Footer as DialogFooter,
Header as DialogHeader,
Trigger as DialogTrigger,
Overlay as DialogOverlay,
Content as DialogContent,
Description as DialogDescription,
Close as DialogClose,
};

@ -14,7 +14,7 @@
<input
bind:this={ref}
class={cn(
"border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
"border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-base file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
bind:value

@ -0,0 +1,36 @@
import { Dialog as SheetPrimitive } from "bits-ui";
import Overlay from "./sheet-overlay.svelte";
import Content from "./sheet-content.svelte";
import Header from "./sheet-header.svelte";
import Footer from "./sheet-footer.svelte";
import Title from "./sheet-title.svelte";
import Description from "./sheet-description.svelte";
const Root = SheetPrimitive.Root;
const Close = SheetPrimitive.Close;
const Trigger = SheetPrimitive.Trigger;
const Portal = SheetPrimitive.Portal;
export {
Root,
Close,
Trigger,
Portal,
Overlay,
Content,
Header,
Footer,
Title,
Description,
//
Root as Sheet,
Close as SheetClose,
Trigger as SheetTrigger,
Portal as SheetPortal,
Overlay as SheetOverlay,
Content as SheetContent,
Header as SheetHeader,
Footer as SheetFooter,
Title as SheetTitle,
Description as SheetDescription,
};

@ -0,0 +1,53 @@
<script lang="ts" module>
import { tv, type VariantProps } from "tailwind-variants";
export const sheetVariants = tv({
base: "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 gap-4 p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
variants: {
side: {
top: "data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 border-b",
bottom: "data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 border-t",
left: "data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
right: "data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
},
},
defaultVariants: {
side: "right",
},
});
export type Side = VariantProps<typeof sheetVariants>["side"];
</script>
<script lang="ts">
import { Dialog as SheetPrimitive, type WithoutChildrenOrChild } from "bits-ui";
import X from "lucide-svelte/icons/x";
import type { Snippet } from "svelte";
import SheetOverlay from "./sheet-overlay.svelte";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
side = "right",
portalProps,
children,
...restProps
}: WithoutChildrenOrChild<SheetPrimitive.ContentProps> & {
portalProps?: SheetPrimitive.PortalProps;
side?: Side;
children: Snippet;
} = $props();
</script>
<SheetPrimitive.Portal {...portalProps}>
<SheetOverlay />
<SheetPrimitive.Content bind:ref class={cn(sheetVariants({ side }), className)} {...restProps}>
{@render children?.()}
<SheetPrimitive.Close
class="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none"
>
<X class="size-4" />
<span class="sr-only">Close</span>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPrimitive.Portal>

@ -0,0 +1,16 @@
<script lang="ts">
import { Dialog as SheetPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: SheetPrimitive.DescriptionProps = $props();
</script>
<SheetPrimitive.Description
bind:ref
class={cn("text-muted-foreground text-sm", className)}
{...restProps}
/>

@ -0,0 +1,20 @@
<script lang="ts">
import type { WithElementRef } from "bits-ui";
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
class={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
{...restProps}
>
{@render children?.()}
</div>

@ -0,0 +1,20 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import type { WithElementRef } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
class={cn("flex flex-col space-y-2 text-center sm:text-left", className)}
{...restProps}
>
{@render children?.()}
</div>

@ -0,0 +1,21 @@
<script lang="ts">
import { Dialog as SheetPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: SheetPrimitive.OverlayProps = $props();
export { className as class };
</script>
<SheetPrimitive.Overlay
bind:ref
class={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80",
className
)}
{...restProps}
/>

@ -0,0 +1,16 @@
<script lang="ts">
import { Dialog as SheetPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: SheetPrimitive.TitleProps = $props();
</script>
<SheetPrimitive.Title
bind:ref
class={cn("text-foreground text-lg font-semibold", className)}
{...restProps}
/>

@ -0,0 +1,6 @@
export const SIDEBAR_COOKIE_NAME = "sidebar:state";
export const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
export const SIDEBAR_WIDTH = "16rem";
export const SIDEBAR_WIDTH_MOBILE = "18rem";
export const SIDEBAR_WIDTH_ICON = "3rem";
export const SIDEBAR_KEYBOARD_SHORTCUT = "b";

@ -0,0 +1,81 @@
import { IsMobile } from "$lib/hooks/is-mobile.svelte.js";
import { getContext, setContext } from "svelte";
import { SIDEBAR_KEYBOARD_SHORTCUT } from "./constants.js";
type Getter<T> = () => T;
export type SidebarStateProps = {
/**
* A getter function that returns the current open state of the sidebar.
* We use a getter function here to support `bind:open` on the `Sidebar.Provider`
* component.
*/
open: Getter<boolean>;
/**
* A function that sets the open state of the sidebar. To support `bind:open`, we need
* a source of truth for changing the open state to ensure it will be synced throughout
* the sub-components and any `bind:` references.
*/
setOpen: (open: boolean) => void;
};
class SidebarState {
readonly props: SidebarStateProps;
open = $derived.by(() => this.props.open());
openMobile = $state(false);
setOpen: SidebarStateProps["setOpen"];
#isMobile: IsMobile;
state = $derived.by(() => (this.open ? "expanded" : "collapsed"));
constructor(props: SidebarStateProps) {
this.setOpen = props.setOpen;
this.#isMobile = new IsMobile();
this.props = props;
}
// Convenience getter for checking if the sidebar is mobile
// without this, we would need to use `sidebar.isMobile.current` everywhere
get isMobile() {
return this.#isMobile.current;
}
// Event handler to apply to the `<svelte:window>`
handleShortcutKeydown = (e: KeyboardEvent) => {
if (e.key === SIDEBAR_KEYBOARD_SHORTCUT && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
this.toggle();
}
};
setOpenMobile = (value: boolean) => {
this.openMobile = value;
};
toggle = () => {
return this.#isMobile.current
? (this.openMobile = !this.openMobile)
: this.setOpen(!this.open);
};
}
const SYMBOL_KEY = "scn-sidebar";
/**
* Instantiates a new `SidebarState` instance and sets it in the context.
*
* @param props The constructor props for the `SidebarState` class.
* @returns The `SidebarState` instance.
*/
export function setSidebar(props: SidebarStateProps): SidebarState {
return setContext(Symbol.for(SYMBOL_KEY), new SidebarState(props));
}
/**
* Retrieves the `SidebarState` instance from the context. This is a class instance,
* so you cannot destructure it.
* @returns The `SidebarState` instance.
*/
export function useSidebar(): SidebarState {
return getContext(Symbol.for(SYMBOL_KEY));
}

@ -0,0 +1,75 @@
import { useSidebar } from "./context.svelte.js";
import Content from "./sidebar-content.svelte";
import Footer from "./sidebar-footer.svelte";
import GroupAction from "./sidebar-group-action.svelte";
import GroupContent from "./sidebar-group-content.svelte";
import GroupLabel from "./sidebar-group-label.svelte";
import Group from "./sidebar-group.svelte";
import Header from "./sidebar-header.svelte";
import Input from "./sidebar-input.svelte";
import Inset from "./sidebar-inset.svelte";
import MenuAction from "./sidebar-menu-action.svelte";
import MenuBadge from "./sidebar-menu-badge.svelte";
import MenuButton from "./sidebar-menu-button.svelte";
import MenuItem from "./sidebar-menu-item.svelte";
import MenuSkeleton from "./sidebar-menu-skeleton.svelte";
import MenuSubButton from "./sidebar-menu-sub-button.svelte";
import MenuSubItem from "./sidebar-menu-sub-item.svelte";
import MenuSub from "./sidebar-menu-sub.svelte";
import Menu from "./sidebar-menu.svelte";
import Provider from "./sidebar-provider.svelte";
import Rail from "./sidebar-rail.svelte";
import Separator from "./sidebar-separator.svelte";
import Trigger from "./sidebar-trigger.svelte";
import Root from "./sidebar.svelte";
export {
Content,
Footer,
Group,
GroupAction,
GroupContent,
GroupLabel,
Header,
Input,
Inset,
Menu,
MenuAction,
MenuBadge,
MenuButton,
MenuItem,
MenuSkeleton,
MenuSub,
MenuSubButton,
MenuSubItem,
Provider,
Rail,
Root,
Separator,
//
Root as Sidebar,
Content as SidebarContent,
Footer as SidebarFooter,
Group as SidebarGroup,
GroupAction as SidebarGroupAction,
GroupContent as SidebarGroupContent,
GroupLabel as SidebarGroupLabel,
Header as SidebarHeader,
Input as SidebarInput,
Inset as SidebarInset,
Menu as SidebarMenu,
MenuAction as SidebarMenuAction,
MenuBadge as SidebarMenuBadge,
MenuButton as SidebarMenuButton,
MenuItem as SidebarMenuItem,
MenuSkeleton as SidebarMenuSkeleton,
MenuSub as SidebarMenuSub,
MenuSubButton as SidebarMenuSubButton,
MenuSubItem as SidebarMenuSubItem,
Provider as SidebarProvider,
Rail as SidebarRail,
Separator as SidebarSeparator,
Trigger as SidebarTrigger,
Trigger,
useSidebar,
};

@ -0,0 +1,24 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import type { WithElementRef } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
</script>
<div
bind:this={ref}
data-sidebar="content"
class={cn(
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
className
)}
{...restProps}
>
{@render children?.()}
</div>

@ -0,0 +1,21 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import type { WithElementRef } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
</script>
<div
bind:this={ref}
data-sidebar="footer"
class={cn("flex flex-col gap-2 p-2", className)}
{...restProps}
>
{@render children?.()}
</div>

@ -0,0 +1,36 @@
<script lang="ts">
import { cn } from "$lib/utils.js";
import type { WithElementRef } from "bits-ui";
import type { Snippet } from "svelte";
import type { HTMLButtonAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
child,
...restProps
}: WithElementRef<HTMLButtonAttributes> & {
child?: Snippet<[{ props: Record<string, unknown> }]>;
} = $props();
const propObj = $derived({
class: cn(
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-none transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 after:md:hidden",
"group-data-[collapsible=icon]:hidden",
className
),
"data-sidebar": "group-action",
...restProps,
});
</script>
{#if child}
{@render child({ props: propObj })}
{:else}
<button bind:this={ref} {...propObj}>
{@render children?.()}
</button>
{/if}

@ -0,0 +1,21 @@
<script lang="ts">
import { cn } from "$lib/utils.js";
import type { WithElementRef } from "bits-ui";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-sidebar="group-content"
class={cn("w-full text-sm", className)}
{...restProps}
>
{@render children?.()}
</div>

@ -0,0 +1,34 @@
<script lang="ts">
import { cn } from "$lib/utils.js";
import type { WithElementRef } from "bits-ui";
import type { Snippet } from "svelte";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
children,
child,
class: className,
...restProps
}: WithElementRef<HTMLAttributes<HTMLElement>> & {
child?: Snippet<[{ props: Record<string, unknown> }]>;
} = $props();
const mergedProps = $derived({
class: cn(
"text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-none transition-[margin,opa] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
className
),
"data-sidebar": "group-label",
...restProps,
});
</script>
{#if child}
{@render child({ props: mergedProps })}
{:else}
<div bind:this={ref} {...mergedProps}>
{@render children?.()}
</div>
{/if}

@ -0,0 +1,21 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import type { WithElementRef } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
</script>
<div
bind:this={ref}
data-sidebar="group"
class={cn("relative flex w-full min-w-0 flex-col p-2", className)}
{...restProps}
>
{@render children?.()}
</div>

@ -0,0 +1,21 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import type { WithElementRef } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
</script>
<div
bind:this={ref}
data-sidebar="header"
class={cn("flex flex-col gap-2 p-2", className)}
{...restProps}
>
{@render children?.()}
</div>

@ -0,0 +1,23 @@
<script lang="ts">
import type { ComponentProps } from "svelte";
import { Input } from "$lib/components/ui/input/index.js";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
value = $bindable(""),
class: className,
...restProps
}: ComponentProps<typeof Input> = $props();
</script>
<Input
bind:ref
bind:value
data-sidebar="input"
class={cn(
"bg-background focus-visible:ring-sidebar-ring h-8 w-full shadow-none focus-visible:ring-2",
className
)}
{...restProps}
/>

@ -0,0 +1,24 @@
<script lang="ts">
import { cn } from "$lib/utils.js";
import type { WithElementRef } from "bits-ui";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
</script>
<main
bind:this={ref}
class={cn(
"bg-background relative flex min-h-svh flex-1 flex-col",
"peer-data-[variant=inset]:min-h-[calc(100svh-theme(spacing.4))] md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow",
className
)}
{...restProps}
>
{@render children?.()}
</main>

@ -0,0 +1,43 @@
<script lang="ts">
import { cn } from "$lib/utils.js";
import type { WithElementRef } from "bits-ui";
import type { Snippet } from "svelte";
import type { HTMLButtonAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
showOnHover = false,
children,
child,
...restProps
}: WithElementRef<HTMLButtonAttributes> & {
child?: Snippet<[{ props: Record<string, unknown> }]>;
showOnHover?: boolean;
} = $props();
const mergedProps = $derived({
class: cn(
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute right-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-none transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 after:md:hidden",
"peer-data-[size=sm]/menu-button:top-1",
"peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden",
showOnHover &&
"peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0",
className
),
"data-sidebar": "menu-action",
...restProps,
});
</script>
{#if child}
{@render child({ props: mergedProps })}
{:else}
<button bind:this={ref} {...mergedProps}>
{@render children?.()}
</button>
{/if}

@ -0,0 +1,29 @@
<script lang="ts">
import { cn } from "$lib/utils.js";
import type { WithElementRef } from "bits-ui";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
</script>
<div
bind:this={ref}
data-sidebar="menu-badge"
class={cn(
"text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 select-none items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums",
"peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
"peer-data-[size=sm]/menu-button:top-1",
"peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden",
className
)}
{...restProps}
>
{@render children?.()}
</div>

@ -0,0 +1,97 @@
<script lang="ts" module>
import { tv, type VariantProps } from "tailwind-variants";
export const sidebarMenuButtonVariants = tv({
base: "peer/menu-button ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none transition-[width,height,padding] focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:font-medium group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
variants: {
variant: {
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
outline:
"bg-background hover:bg-sidebar-accent hover:text-sidebar-accent-foreground shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
},
size: {
default: "h-8 text-sm",
sm: "h-7 text-xs",
lg: "h-12 text-sm group-data-[collapsible=icon]:!p-0",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
});
export type SidebarMenuButtonVariant = VariantProps<
typeof sidebarMenuButtonVariants
>["variant"];
export type SidebarMenuButtonSize = VariantProps<typeof sidebarMenuButtonVariants>["size"];
</script>
<script lang="ts">
import * as Tooltip from "$lib/components/ui/tooltip/index.js";
import { cn } from "$lib/utils.js";
import { mergeProps, type WithElementRef, type WithoutChildrenOrChild } from "bits-ui";
import type { ComponentProps, Snippet } from "svelte";
import type { HTMLAttributes } from "svelte/elements";
import { useSidebar } from "./context.svelte.js";
let {
ref = $bindable(null),
class: className,
children,
child,
variant = "default",
size = "default",
isActive = false,
tooltipContent,
tooltipContentProps,
...restProps
}: WithElementRef<HTMLAttributes<HTMLButtonElement>, HTMLButtonElement> & {
isActive?: boolean;
variant?: SidebarMenuButtonVariant;
size?: SidebarMenuButtonSize;
tooltipContent?: Snippet;
tooltipContentProps?: WithoutChildrenOrChild<ComponentProps<typeof Tooltip.Content>>;
child?: Snippet<[{ props: Record<string, unknown> }]>;
} = $props();
const sidebar = useSidebar();
const buttonProps = $derived({
class: cn(sidebarMenuButtonVariants({ variant, size }), className),
"data-sidebar": "menu-button",
"data-size": size,
"data-active": isActive,
...restProps,
});
</script>
{#snippet Button({ props }: { props?: Record<string, unknown> })}
{@const mergedProps = mergeProps(buttonProps, props)}
{#if child}
{@render child({ props: mergedProps })}
{:else}
<button bind:this={ref} {...mergedProps}>
{@render children?.()}
</button>
{/if}
{/snippet}
{#if !tooltipContent}
{@render Button({})}
{:else}
<Tooltip.Root>
<Tooltip.Trigger>
{#snippet child({ props })}
{@render Button({ props })}
{/snippet}
</Tooltip.Trigger>
<Tooltip.Content
side="right"
align="center"
hidden={sidebar.state !== "collapsed" || sidebar.isMobile}
children={tooltipContent}
{...tooltipContentProps}
/>
</Tooltip.Root>
{/if}

@ -0,0 +1,21 @@
<script lang="ts">
import { cn } from "$lib/utils.js";
import type { WithElementRef } from "bits-ui";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLLIElement>, HTMLLIElement> = $props();
</script>
<li
bind:this={ref}
data-sidebar="menu-item"
class={cn("group/menu-item relative", className)}
{...restProps}
>
{@render children?.()}
</li>

@ -0,0 +1,36 @@
<script lang="ts">
import { Skeleton } from "$lib/components/ui/skeleton/index.js";
import { cn } from "$lib/utils.js";
import type { WithElementRef } from "bits-ui";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
showIcon = false,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLElement>> & {
showIcon?: boolean;
} = $props();
// Random width between 50% and 90%
const width = `${Math.floor(Math.random() * 40) + 50}%`;
</script>
<div
bind:this={ref}
data-sidebar="menu-skeleton"
class={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
{...restProps}
>
{#if showIcon}
<Skeleton class="size-4 rounded-md" data-sidebar="menu-skeleton-icon" />
{/if}
<Skeleton
class="h-4 max-w-[--skeleton-width] flex-1"
data-sidebar="menu-skeleton-text"
style="--skeleton-width: {width};"
/>
{@render children?.()}
</div>

@ -0,0 +1,43 @@
<script lang="ts">
import { cn } from "$lib/utils.js";
import type { WithElementRef } from "bits-ui";
import type { Snippet } from "svelte";
import type { HTMLAnchorAttributes } from "svelte/elements";
let {
ref = $bindable(null),
children,
child,
class: className,
size = "md",
isActive,
...restProps
}: WithElementRef<HTMLAnchorAttributes> & {
child?: Snippet<[{ props: Record<string, unknown> }]>;
size?: "sm" | "md";
isActive?: boolean;
} = $props();
const mergedProps = $derived({
class: cn(
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-none focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
size === "sm" && "text-xs",
size === "md" && "text-sm",
"group-data-[collapsible=icon]:hidden",
className
),
"data-sidebar": "menu-sub-button",
"data-size": size,
"data-active": isActive,
...restProps,
});
</script>
{#if child}
{@render child({ props: mergedProps })}
{:else}
<a bind:this={ref} {...mergedProps}>
{@render children?.()}
</a>
{/if}

@ -0,0 +1,14 @@
<script lang="ts">
import type { WithElementRef } from "bits-ui";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLLIElement>> = $props();
</script>
<li bind:this={ref} data-sidebar="menu-sub-item" {...restProps}>
{@render children?.()}
</li>

@ -0,0 +1,25 @@
<script lang="ts">
import { cn } from "$lib/utils.js";
import type { WithElementRef } from "bits-ui";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLUListElement>> = $props();
</script>
<ul
bind:this={ref}
data-sidebar="menu-sub"
class={cn(
"border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5",
"group-data-[collapsible=icon]:hidden",
className
)}
{...restProps}
>
{@render children?.()}
</ul>

@ -0,0 +1,21 @@
<script lang="ts">
import { cn } from "$lib/utils.js";
import type { WithElementRef } from "bits-ui";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLUListElement>, HTMLUListElement> = $props();
</script>
<ul
bind:this={ref}
data-sidebar="menu"
class={cn("flex w-full min-w-0 flex-col gap-1", className)}
{...restProps}
>
{@render children?.()}
</ul>

@ -0,0 +1,53 @@
<script lang="ts">
import * as Tooltip from "$lib/components/ui/tooltip/index.js";
import { cn } from "$lib/utils.js";
import type { WithElementRef } from "bits-ui";
import type { HTMLAttributes } from "svelte/elements";
import {
SIDEBAR_COOKIE_MAX_AGE,
SIDEBAR_COOKIE_NAME,
SIDEBAR_WIDTH,
SIDEBAR_WIDTH_ICON,
} from "./constants.js";
import { setSidebar } from "./context.svelte.js";
let {
ref = $bindable(null),
open = $bindable(true),
onOpenChange = () => {},
class: className,
style,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
open?: boolean;
onOpenChange?: (open: boolean) => void;
} = $props();
const sidebar = setSidebar({
open: () => open,
setOpen: (value: boolean) => {
open = value;
onOpenChange(value);
// This sets the cookie to keep the sidebar state.
document.cookie = `${SIDEBAR_COOKIE_NAME}=${open}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
},
});
</script>
<svelte:window onkeydown={sidebar.handleShortcutKeydown} />
<Tooltip.Provider delayDuration={0}>
<div
style="--sidebar-width: {SIDEBAR_WIDTH}; --sidebar-width-icon: {SIDEBAR_WIDTH_ICON}; {style}"
class={cn(
"group/sidebar-wrapper has-[[data-variant=inset]]:bg-sidebar flex min-h-svh w-full",
className
)}
bind:this={ref}
{...restProps}
>
{@render children?.()}
</div>
</Tooltip.Provider>

@ -0,0 +1,36 @@
<script lang="ts">
import { cn } from "$lib/utils.js";
import type { WithElementRef } from "bits-ui";
import type { HTMLAttributes } from "svelte/elements";
import { useSidebar } from "./context.svelte.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLButtonElement>, HTMLButtonElement> = $props();
const sidebar = useSidebar();
</script>
<button
bind:this={ref}
data-sidebar="rail"
aria-label="Toggle Sidebar"
tabIndex={-1}
onclick={() => sidebar.toggle()}
title="Toggle Sidebar"
class={cn(
"hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex",
"[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize",
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
"group-data-[collapsible=offcanvas]:hover:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full",
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
className
)}
{...restProps}
>
{@render children?.()}
</button>

@ -0,0 +1,18 @@
<script lang="ts">
import { Separator } from "$lib/components/ui/separator/index.js";
import { cn } from "$lib/utils.js";
import type { ComponentProps } from "svelte";
let {
ref = $bindable(null),
class: className,
...restProps
}: ComponentProps<typeof Separator> = $props();
</script>
<Separator
bind:ref
data-sidebar="separator"
class={cn("bg-sidebar-border mx-2 w-auto", className)}
{...restProps}
/>

@ -0,0 +1,34 @@
<script lang="ts">
import { Button } from "$lib/components/ui/button/index.js";
import { cn } from "$lib/utils.js";
import PanelLeft from "lucide-svelte/icons/panel-left";
import type { ComponentProps } from "svelte";
import { useSidebar } from "./context.svelte.js";
let {
ref = $bindable(null),
class: className,
onclick,
...restProps
}: ComponentProps<typeof Button> & {
onclick?: (e: MouseEvent) => void;
} = $props();
const sidebar = useSidebar();
</script>
<Button
type="button"
onclick={(e) => {
onclick?.(e);
sidebar.toggle();
}}
data-sidebar="trigger"
variant="ghost"
size="icon"
class={cn("h-7 w-7", className)}
{...restProps}
>
<PanelLeft />
<span class="sr-only">Toggle Sidebar</span>
</Button>

@ -0,0 +1,96 @@
<script lang="ts">
import * as Sheet from "$lib/components/ui/sheet/index.js";
import { cn } from "$lib/utils.js";
import type { WithElementRef } from "bits-ui";
import type { HTMLAttributes } from "svelte/elements";
import { SIDEBAR_WIDTH_MOBILE } from "./constants.js";
import { useSidebar } from "./context.svelte.js";
let {
ref = $bindable(null),
side = "left",
variant = "sidebar",
collapsible = "offcanvas",
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
side?: "left" | "right";
variant?: "sidebar" | "floating" | "inset";
collapsible?: "offcanvas" | "icon" | "none";
} = $props();
const sidebar = useSidebar();
</script>
{#if collapsible === "none"}
<div
class={cn(
"bg-sidebar text-sidebar-foreground flex h-full w-[--sidebar-width] flex-col",
className
)}
bind:this={ref}
{...restProps}
>
{@render children?.()}
</div>
{:else if sidebar.isMobile}
<Sheet.Root
bind:open={() => sidebar.openMobile, (v) => sidebar.setOpenMobile(v)}
{...restProps}
>
<Sheet.Content
data-sidebar="sidebar"
data-mobile="true"
class="bg-sidebar text-sidebar-foreground w-[--sidebar-width] p-0 [&>button]:hidden"
style="--sidebar-width: {SIDEBAR_WIDTH_MOBILE};"
{side}
>
<div class="flex h-full w-full flex-col">
{@render children?.()}
</div>
</Sheet.Content>
</Sheet.Root>
{:else}
<div
bind:this={ref}
class="text-sidebar-foreground group peer hidden md:block"
data-state={sidebar.state}
data-collapsible={sidebar.state === "collapsed" ? collapsible : ""}
data-variant={variant}
data-side={side}
>
<!-- This is what handles the sidebar gap on desktop -->
<div
class={cn(
"relative h-svh w-[--sidebar-width] bg-transparent transition-[width] duration-200 ease-linear",
"group-data-[collapsible=offcanvas]:w-0",
"group-data-[side=right]:rotate-180",
variant === "floating" || variant === "inset"
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]"
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon]"
)}
></div>
<div
class={cn(
"fixed inset-y-0 z-10 hidden h-svh w-[--sidebar-width] transition-[left,right,width] duration-200 ease-linear md:flex",
side === "left"
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
// Adjust the padding for floating and inset variants.
variant === "floating" || variant === "inset"
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]"
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[side=left]:border-r group-data-[side=right]:border-l",
className
)}
{...restProps}
>
<div
data-sidebar="sidebar"
class="bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow"
>
{@render children?.()}
</div>
</div>
</div>
{/if}

@ -0,0 +1,7 @@
import Root from "./skeleton.svelte";
export {
Root,
//
Root as Skeleton,
};

@ -0,0 +1,17 @@
<script lang="ts">
import type { WithElementRef, WithoutChildren } from "bits-ui";
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: WithoutChildren<WithElementRef<HTMLAttributes<HTMLDivElement>>> = $props();
</script>
<div
bind:this={ref}
class={cn("bg-muted animate-pulse rounded-md", className)}
{...restProps}
></div>

@ -0,0 +1,7 @@
import Root from "./slider.svelte";
export {
Root,
//
Root as Slider,
};

@ -0,0 +1,34 @@
<script lang="ts">
import { Slider as SliderPrimitive, type WithoutChildrenOrChild } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
value = $bindable(),
class: className,
...restProps
}: WithoutChildrenOrChild<SliderPrimitive.RootProps> = $props();
</script>
<!--
Discriminated Unions + Destructing (required for bindable) do not
get along, so we shut typescript up by casting `value` to `never`.
-->
<SliderPrimitive.Root
bind:value={value as never}
bind:ref
class={cn("relative flex w-full touch-none select-none items-center", className)}
{...restProps}
>
{#snippet children({ thumbs })}
<span class="bg-secondary relative h-2 w-full grow overflow-hidden rounded-full">
<SliderPrimitive.Range class="bg-primary absolute h-full" />
</span>
{#each thumbs as thumb}
<SliderPrimitive.Thumb
index={thumb}
class="border-primary bg-background ring-offset-background focus-visible:ring-ring block size-5 rounded-full border-2 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50"
/>
{/each}
{/snippet}
</SliderPrimitive.Root>

@ -0,0 +1 @@
export { default as Toaster } from "./sonner.svelte";

@ -0,0 +1,20 @@
<script lang="ts">
import { Toaster as Sonner, type ToasterProps as SonnerProps } from "svelte-sonner";
import { mode } from "mode-watcher";
let restProps: SonnerProps = $props();
</script>
<Sonner
theme={$mode}
class="toaster group"
toastOptions={{
classes: {
toast: "group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
description: "group-[.toast]:text-muted-foreground",
actionButton: "group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
cancelButton: "group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
},
}}
{...restProps}
/>

@ -0,0 +1,18 @@
import { Tooltip as TooltipPrimitive } from "bits-ui";
import Content from "./tooltip-content.svelte";
const Root = TooltipPrimitive.Root;
const Trigger = TooltipPrimitive.Trigger;
const Provider = TooltipPrimitive.Provider;
export {
Root,
Trigger,
Content,
Provider,
//
Root as Tooltip,
Content as TooltipContent,
Trigger as TooltipTrigger,
Provider as TooltipProvider,
};

@ -0,0 +1,21 @@
<script lang="ts">
import { Tooltip as TooltipPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
sideOffset = 4,
...restProps
}: TooltipPrimitive.ContentProps = $props();
</script>
<TooltipPrimitive.Content
bind:ref
{sideOffset}
class={cn(
"bg-popover text-popover-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 overflow-hidden rounded-md border px-3 py-1.5 text-sm shadow-md",
className
)}
{...restProps}
/>

@ -0,0 +1,9 @@
// Make sure to install the 'pg' package
import { drizzle } from 'drizzle-orm/bun-sql'
import * as schema from './schema'
import { env } from '$env/dynamic/private'
if (!env.DATABASE_URL) throw new Error('DATABASE_URL is not set')
const client = new Bun.SQL(env.DATABASE_URL)
export const db = drizzle(client, { schema })

@ -0,0 +1,38 @@
import {
pgTable,
varchar,
text,
bigint,
json,
integer,
} from 'drizzle-orm/pg-core'
import { relations, type InferSelectModel } from 'drizzle-orm'
import type { z } from 'zod'
export const user = pgTable('user', {
id: text('id').primaryKey(),
email: varchar('email', { length: 255 }).notNull(),
passwordHash: varchar('password_hash', { length: 255 }).notNull(),
createdAt: bigint('created_at', { mode: 'number' }).notNull(),
})
export const session = pgTable('session', {
id: text('id').primaryKey(),
userId: text('user_id').notNull(),
expiresAt: bigint('expires_at_epoch', { mode: 'number' }).notNull(),
})
export const bybitWebhook = pgTable('bybit_webhook', {
id: text('id').primaryKey(),
userId: text('user_id').notNull(),
direction: varchar('direction', { length: 255 })
.$type<'buy' | 'sell'>()
.notNull(),
symbol: varchar('symbol', { length: 255 }).notNull(),
quantityPercent: integer('quantity_percent').notNull(),
takeProfitPercent: integer('takeprofit_percent').notNull(),
stopLossPercent: integer('stoploss_percent').notNull(),
})
export type Session = InferSelectModel<typeof session>
export type BybitWebhook = InferSelectModel<typeof bybitWebhook>

@ -0,0 +1,27 @@
import { untrack } from "svelte";
const MOBILE_BREAKPOINT = 768;
export class IsMobile {
#current = $state<boolean>(false);
constructor() {
$effect(() => {
return untrack(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
const onChange = () => {
this.#current = window.innerWidth < MOBILE_BREAKPOINT;
};
mql.addEventListener("change", onChange);
onChange();
return () => {
mql.removeEventListener("change", onChange);
};
});
});
}
get current() {
return this.#current;
}
}

@ -0,0 +1,96 @@
import type { RequestEvent } from '@sveltejs/kit'
import { eq } from 'drizzle-orm'
import { sha256 } from '@oslojs/crypto/sha2'
import { encodeBase64url, encodeHexLowerCase } from '@oslojs/encoding'
import { db } from '$lib/db'
import * as table from '$lib/db/schema'
const DAY_IN_MS = 1000 * 60 * 60 * 24
export const sessionCookieName = 'auth-session'
export function generateSessionToken() {
const bytes = crypto.getRandomValues(new Uint8Array(18))
const token = encodeBase64url(bytes)
return token
}
export async function createSession(token: string, userId: string) {
const sessionId = encodeHexLowerCase(
sha256(new TextEncoder().encode(token)),
)
const session: table.Session = {
id: sessionId,
userId,
expiresAt: Date.now() + DAY_IN_MS * 30,
}
await db.insert(table.session).values(session)
return session
}
export async function validateSessionToken(token: string) {
const sessionId = encodeHexLowerCase(
sha256(new TextEncoder().encode(token)),
)
const [result] = await db
.select({
// Adjust user table here to tweak returned data
user: table.user,
session: table.session,
})
.from(table.session)
.innerJoin(table.user, eq(table.session.userId, table.user.id))
.where(eq(table.session.id, sessionId))
if (!result) {
return { session: null, user: null }
}
const { session, user } = result
const sessionExpired = Date.now() >= session.expiresAt
if (sessionExpired) {
await db
.delete(table.session)
.where(eq(table.session.id, session.id))
return { session: null, user: null }
}
const renewSession =
Date.now() >= session.expiresAt - DAY_IN_MS * 15
if (renewSession) {
session.expiresAt = Date.now() + DAY_IN_MS * 30
await db
.update(table.session)
.set({ expiresAt: session.expiresAt })
.where(eq(table.session.id, session.id))
}
return { session, user }
}
export type SessionValidationResult = Awaited<
ReturnType<typeof validateSessionToken>
>
export async function invalidateSession(sessionId: string) {
await db
.delete(table.session)
.where(eq(table.session.id, sessionId))
}
export function setSessionTokenCookie(
event: RequestEvent,
token: string,
expiresAt: Date,
) {
event.cookies.set(sessionCookieName, token, {
expires: expiresAt,
path: '/',
})
}
export function deleteSessionTokenCookie(event: RequestEvent) {
event.cookies.delete(sessionCookieName, {
path: '/',
})
}

@ -0,0 +1,8 @@
import { encodeBase32LowerCase } from '@oslojs/encoding'
export function generateId() {
// ID with 120 bits of entropy, or about the same as UUID v4.
const bytes = crypto.getRandomValues(new Uint8Array(15))
const id = encodeBase32LowerCase(bytes)
return id
}

@ -0,0 +1,86 @@
export const chains = [
{ name: 'Ethereum', chainId: 1 },
{ name: 'Binance Smart Chain', chainId: 56 },
{ name: 'Polygon', chainId: 137 },
{ name: 'Arbirum One', chainId: 42161 },
] as const
export type Token = {
name: string
symbol: string
decimal: number
icon: string
address: string
}
export const tokens = {
1: [],
56: [
{
name: 'Binance-Peg Ethereum Token',
symbol: 'ETH',
decimal: 18,
icon: 'ETH.png',
address: '0x2170Ed0880ac9A755fd29B2688956BD959F933F8',
},
{
name: 'Binance-Peg XRP Token',
symbol: 'XRP',
decimal: 18,
icon: 'XRP.png',
address: '0x1D2F0da169ceB9fC7B3144628dB156f3F6c60dBE',
},
{
name: 'Binance-Peg BSC-USD',
symbol: 'BSC-USD',
decimal: 18,
icon: 'BSC-USD.png',
address: '0x55d398326f99059fF775485246999027B3197955',
},
{
name: 'Binance-Peg USD Coin',
symbol: 'USDC',
decimal: 18,
icon: 'USDC.png',
address: '0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d',
},
{
name: 'Thunder Wrapped BNB',
symbol: 'BNB',
decimal: 18,
icon: 'BNB.png',
address: '0x3E14602186DD9dE538F729547B3918D24c823546',
},
{
name: 'Wrapped BNB',
symbol: 'WBNB',
decimal: 18,
icon: 'WBNB.png',
address: '0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c',
},
{
name: 'Binance-Peg Dogecoin Token',
symbol: 'DOGE',
decimal: 8,
icon: 'DOGE.png',
address: '0xbA2aE424d960c26247Dd6c32edC70B295c744C43',
},
],
137: [
{
name: 'USDC',
decimal: 6,
address: '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359',
},
{
name: 'USD Coin (PoS) (USDC.e)',
decimal: 6,
address: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174',
},
{
name: '(PoS) Tether USD (USDT)',
decimal: 6,
address: '0xc2132D05D31c914a87C6611C10748AEb04B58e8F',
},
],
42161: [],
} as const

@ -0,0 +1,7 @@
import { redirect } from '@sveltejs/kit'
import type { PageServerLoad } from './$types'
export const load = (async () => {
redirect(302, '/dashboard/crypto-data')
return {}
}) satisfies PageServerLoad

@ -1,311 +0,0 @@
<script lang="ts">
import * as Form from '$lib/components/ui/form'
import * as Select from '$lib/components/ui/select'
import { Input } from '$lib/components/ui/input'
import { superForm } from 'sveltekit-superforms'
import { zod } from 'sveltekit-superforms/adapters'
import { formSchema, intervals } from './schema'
import { categories } from './schema'
import { CalendarIcon } from 'lucide-svelte'
import { Calendar } from '$lib/components/ui/calendar/index.js'
import * as Popover from '$lib/components/ui/popover/index.js'
import * as Card from '$lib/components/ui/card/index.js'
import * as Tabs from '$lib/components/ui/tabs/index.js'
import {
CalendarDate,
DateFormatter,
type DateValue,
getLocalTimeZone,
parseDate,
today,
} from '@internationalized/date'
import { cn } from '$lib/utils.js'
import { buttonVariants } from '$lib/components/ui/button'
import Bingx from './(components)/bingx.svelte'
import { ScrollArea } from '$lib/components/ui/scroll-area'
let { data } = $props()
const apiEndpoint = 'https://api.bybit.com/v5/market/kline'
const form = superForm(data.form, {
resetForm: false,
SPA: true,
validators: zod(formSchema),
onUpdated: async ({ form }) => {
if (!form.valid) return
console.log(form.data)
// Form validation
const url = new URL(apiEndpoint)
url.searchParams.set('symbol', form.data.symbol)
url.searchParams.set('interval', form.data.interval)
const start = new Date(
form.data.start + ' ' + form.data.startTime + ':00',
)
url.searchParams.set('start', start.getTime().toString())
const end = new Date(
form.data.end + ' ' + form.data.endTime + ':00',
)
url.searchParams.set('end', end.getTime().toString())
if (form.data.limit)
url.searchParams.set('limit', form.data.limit.toString())
if (form.data.category)
url.searchParams.set('category', form.data.category)
const response = await fetch(url)
const data = await response.json()
console.log(data.result.list)
let csvContent =
'data:text/csv;charset=utf-8,' +
'startTime,openPrice,highPrice,lowPrice,closePrice,volume,turnover\n' +
data.result.list
.map((data: string[]) => {
let newData = data
newData[0] = new Date(parseInt(newData[0]))
.toLocaleString()
.replace(',', ' |')
return newData
})
.map((e: any) => e.join(','))
.join('\n')
var encodedUri = encodeURI(csvContent)
var link = document.createElement('a')
link.setAttribute('href', encodedUri)
link.setAttribute('download', 'data.csv')
document.body.appendChild(link)
link.click()
},
})
const { form: formData, enhance } = form
const df = new DateFormatter('en-US', {
dateStyle: 'long',
})
let start = $state<DateValue | undefined>()
let end = $state<DateValue | undefined>()
$effect(() => {
start = $formData.start ? parseDate($formData.start) : undefined
end = $formData.end ? parseDate($formData.end) : undefined
})
let placeholder = $state<DateValue>(today(getLocalTimeZone()))
</script>
<div class="flex justify-center">
<Card.Root>
<Card.Content>
<Tabs.Root value="bybit" class="w-[400px]">
<Tabs.List>
<Tabs.Trigger value="bybit">ByBit</Tabs.Trigger>
<Tabs.Trigger value="bingx">BingX</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="bybit">
<form method="POST" use:enhance>
<Form.Field {form} name="symbol">
<Form.Control>
{#snippet children({ props })}
<Form.Label>Symbol</Form.Label>
<Input {...props} bind:value={$formData.symbol} />
{/snippet}
</Form.Control>
<Form.Description>Symbol</Form.Description>
<Form.FieldErrors />
</Form.Field>
<Form.Field {form} name="limit">
<Form.Control>
{#snippet children({ props })}
<Form.Label>Limit</Form.Label>
<Input
{...props}
bind:value={$formData.limit}
type="number" />
{/snippet}
</Form.Control>
<Form.Description>Limit</Form.Description>
<Form.FieldErrors />
</Form.Field>
<Form.Field {form} name="category">
<Form.Control>
{#snippet children({ props })}
<Form.Label>Category</Form.Label>
<Select.Root
type="single"
bind:value={$formData.category}
name={props.name}>
<Select.Trigger {...props}>
{$formData.category ?? 'Select a category'}
</Select.Trigger>
<Select.Content>
{#each categories as category}
<Select.Item
value={category}
label={category} />
{/each}
</Select.Content>
</Select.Root>
{/snippet}
</Form.Control>
<Form.Description>Category</Form.Description>
<Form.FieldErrors />
</Form.Field>
<Form.Field {form} name="interval">
<Form.Control>
{#snippet children({ props })}
<Form.Label>Interval</Form.Label>
<Select.Root
type="single"
bind:value={$formData.interval}
name={props.name}>
<Select.Trigger {...props}>
{$formData.interval ?? 'Select a interval'}
</Select.Trigger>
<Select.Content>
{#each intervals as interval}
<Select.Item
value={interval}
label={interval} />
{/each}
</Select.Content>
</Select.Root>
{/snippet}
</Form.Control>
<Form.Description>Interval</Form.Description>
<Form.FieldErrors />
</Form.Field>
<Form.Field {form} name="start" class="flex flex-col">
<Form.Control>
{#snippet children({ props })}
<Form.Label>Start Date</Form.Label>
<Popover.Root>
<Popover.Trigger
{...props}
class={cn(
buttonVariants({ variant: 'outline' }),
'w-[280px] justify-start pl-4 text-left font-normal',
!start && 'text-muted-foreground',
)}>
{start
? df.format(start.toDate(getLocalTimeZone()))
: 'Pick a date'}
<CalendarIcon
class="ml-auto size-4 opacity-50" />
</Popover.Trigger>
<Popover.Content class="w-auto p-0" side="top">
<Calendar
type="single"
value={start as DateValue}
bind:placeholder
minValue={new CalendarDate(1900, 1, 1)}
maxValue={today(getLocalTimeZone())}
calendarLabel="Date of birth"
onValueChange={(v) => {
if (v) {
$formData.start = v.toString()
} else {
$formData.start = ''
}
}} />
</Popover.Content>
</Popover.Root>
<Form.Description>Start Time</Form.Description>
<Form.FieldErrors />
<input
hidden
value={$formData.start}
name={props.name} />
{/snippet}
</Form.Control>
</Form.Field>
<Form.Field {form} name="startTime">
<Form.Control>
{#snippet children({ props })}
<Form.Label>Start Time</Form.Label>
<Input
{...props}
bind:value={$formData.startTime}
type="time" />
{/snippet}
</Form.Control>
<Form.Description>Start Time</Form.Description>
<Form.FieldErrors />
</Form.Field>
<Form.Field {form} name="end" class="flex flex-col">
<Form.Control>
{#snippet children({ props })}
<Form.Label>End Date</Form.Label>
<Popover.Root>
<Popover.Trigger
{...props}
class={cn(
buttonVariants({ variant: 'outline' }),
'w-[280px] justify-start pl-4 text-left font-normal',
!end && 'text-muted-foreground',
)}>
{end
? df.format(end.toDate(getLocalTimeZone()))
: 'Pick a date'}
<CalendarIcon
class="ml-auto size-4 opacity-50" />
</Popover.Trigger>
<Popover.Content class="w-auto p-0" side="top">
<Calendar
type="single"
value={end as DateValue}
bind:placeholder
minValue={new CalendarDate(1900, 1, 1)}
maxValue={today(getLocalTimeZone())}
calendarLabel="Date of birth"
onValueChange={(v) => {
if (v) {
$formData.end = v.toString()
} else {
$formData.end = ''
}
}} />
</Popover.Content>
</Popover.Root>
<Form.Description>End Date</Form.Description>
<Form.FieldErrors />
<input
hidden
value={$formData.end}
name={props.name} />
{/snippet}
</Form.Control>
</Form.Field>
<Form.Field {form} name="endTime">
<Form.Control>
{#snippet children({ props })}
<Form.Label>End Time</Form.Label>
<Input
{...props}
bind:value={$formData.endTime}
type="time" />
{/snippet}
</Form.Control>
<Form.Description>End Time</Form.Description>
<Form.FieldErrors />
</Form.Field>
<Form.Button>Submit</Form.Button>
</form>
</Tabs.Content>
<Tabs.Content value="bingx">
<Bingx />
</Tabs.Content>
</Tabs.Root>
</Card.Content>
</Card.Root>
</div>

@ -0,0 +1,18 @@
import { json } from '@sveltejs/kit'
import { RestClientV5 } from 'bybit-api'
const apiKey = 'wrc1w54Zp5JAfXLxY2'
const apiSecret = 'tY7oYPhwSE1gabFS4PmxtmbDOhkYWvPh0khf'
const client = new RestClientV5({
key: apiKey,
secret: apiSecret,
demoTrading: true,
})
export const GET = async (event) => {
const response = await client.getWalletBalance({
accountType: 'UNIFIED',
})
return json({ response })
}

@ -0,0 +1,60 @@
import { db } from '$lib/db/index.js'
import { bybitWebhook } from '$lib/db/schema.js'
import { generateId } from '$lib/server/utils.js'
import { json } from '@sveltejs/kit'
import { eq } from 'drizzle-orm'
import { z } from 'zod'
export const GET = async ({ locals }) => {
const webhooks = await db
.select()
.from(bybitWebhook)
.where(eq(bybitWebhook.userId, locals.user.id))
return json({ webhooks })
}
const formSchema = z.object({
symbol: z.string(),
side: z.enum(['buy', 'sell']),
qty: z.number().refine((value) => value.toString()),
takeProfit: z.number().refine((value) => value.toString()),
stopLoss: z.number().refine((value) => value.toString()),
})
export const POST = async ({ locals, request }) => {
const form = formSchema.safeParse(await request.json())
if (!form.success) {
return json({}, { status: 400 })
}
console.log(form.data)
const { qty, side, stopLoss, symbol, takeProfit } = form.data
const existingWebhook = await db.query.bybitWebhook.findFirst({
where: (webhook, { eq, and }) =>
and(
eq(webhook.userId, locals.user.id),
eq(webhook.symbol, symbol),
eq(webhook.direction, side),
),
})
if (existingWebhook) {
return json({ message: 'Webhook exist' }, { status: 400 })
}
await db.insert(bybitWebhook).values({
id: generateId(),
direction: side,
quantityPercent: qty,
stopLossPercent: stopLoss,
symbol: symbol,
takeProfitPercent: takeProfit,
userId: locals.user.id,
})
return json({})
}

@ -0,0 +1,60 @@
<script lang="ts">
import * as Sidebar from '$lib/components/ui/sidebar/index.js'
import type { ComponentProps } from 'svelte'
import { page } from '$app/state'
import { CoinsIcon, HomeIcon, SquareTerminal } from 'lucide-svelte'
let {
ref = $bindable(null),
collapsible = 'icon',
...restProps
}: ComponentProps<typeof Sidebar.Root> & {} = $props()
const data = $derived({
navMain: [
{
title: 'Crypto Data',
url: `/dashboard/crypto-data`,
icon: HomeIcon,
isActive: page.url.pathname === `/dashboard/crypto-data`,
},
{
title: 'Arbitrage',
url: `/dashboard/swap`,
icon: SquareTerminal,
isActive: page.url.pathname === `/dashboard/swap`,
},
{
title: 'ByBit',
url: `/dashboard/bybit`,
icon: CoinsIcon,
isActive: page.url.pathname === `/dashboard/bybit`,
},
],
})
</script>
<Sidebar.Root bind:ref {collapsible} {...restProps}>
<Sidebar.Content>
<Sidebar.Group>
<Sidebar.GroupLabel>Dashboard</Sidebar.GroupLabel>
<Sidebar.Menu>
{#each data.navMain as item (item.title)}
<Sidebar.MenuItem>
<Sidebar.MenuButton isActive={item.isActive}>
{#snippet child({ props })}
<a href={item.url} {...props}>
{#if item.icon}
<item.icon class="h-8" />
{/if}
<span>{item.title}</span>
</a>
{/snippet}
</Sidebar.MenuButton>
</Sidebar.MenuItem>
{/each}
</Sidebar.Menu>
</Sidebar.Group>
</Sidebar.Content>
<Sidebar.Rail />
</Sidebar.Root>

@ -0,0 +1,17 @@
<script lang="ts">
import AppSidebar from './(components)/app-sidebar.svelte'
import * as Sidebar from '$lib/components/ui/sidebar/index.js'
let { data, children } = $props()
</script>
<Sidebar.Provider>
<AppSidebar collapsible="offcanvas" />
<Sidebar.Inset>
<header class="flex h-16 shrink-0 items-center gap-2 px-4">
<Sidebar.Trigger class="-ml-1" />
</header>
{@render children()}
</Sidebar.Inset>
</Sidebar.Provider>

@ -0,0 +1,225 @@
<script lang="ts">
import { WebsocketClient } from 'bybit-api'
import * as Form from '$lib/components/ui/form'
import * as Select from '$lib/components/ui/select'
import { Input } from '$lib/components/ui/input'
import { superForm } from 'sveltekit-superforms'
import { zod } from 'sveltekit-superforms/adapters'
import { formSchema, categories } from './schema'
import { CalendarIcon } from 'lucide-svelte'
import { Calendar } from '$lib/components/ui/calendar/index.js'
import * as Popover from '$lib/components/ui/popover/index.js'
import * as Card from '$lib/components/ui/card/index.js'
import { Slider } from '$lib/components/ui/slider/index.js'
import { onMount } from 'svelte'
import type { KLineWSResponse } from './types'
import { Label } from '$lib/components/ui/label'
import { customFetch } from '$lib/client/fetch'
import type { BybitWebhook } from '$lib/db/schema'
let { data } = $props()
const form = superForm(data.form, {
resetForm: false,
SPA: true,
validators: zod(formSchema),
onUpdate: async ({ form, result }) => {
if (result.type == 'success') {
console.log(result.data)
const response = await customFetch('/api/bybit/webhook', {
method: 'POST',
body: JSON.stringify(result.data.form.data),
})
if (response.ok) {
webhooks = await getWebhooks()
}
}
},
})
const getWebhooks = async () => {
const response = await customFetch('/api/bybit/webhook')
return (await response.json()).webhooks as BybitWebhook[]
}
let webhooks = $state<BybitWebhook[]>([])
onMount(async () => {
webhooks = await getWebhooks()
})
const apiKey = 'wrc1w54Zp5JAfXLxY2'
const apiSecret = 'tY7oYPhwSE1gabFS4PmxtmbDOhkYWvPh0khf'
let price = $state<number>()
const handleKLineData = (data: KLineWSResponse) => {
price = Number(data.data[0].close)
}
const createPublicWS = () => {
const client = new WebsocketClient({
market: 'v5',
key: apiKey,
secret: apiSecret,
})
client.on('error', (data) => {
console.error('ws exception: ', data)
})
client.subscribeV5(['kline.1.BTCUSDT'], 'spot')
client.on('open', function () {
console.log('connection open')
})
client.on('update', function (message) {
// console.log('update', message)
if (message.topic === 'kline.1.BTCUSDT') {
handleKLineData(message)
}
})
client.on('response', function (response) {
console.log('response', response)
})
client.on('close', () => {
console.log('connection closed')
})
}
const createPrivateWS = () => {
const client = new WebsocketClient({
market: 'v5',
key: apiKey,
secret: apiSecret,
demoTrading: true,
})
client.on('error', (data) => {
console.error('ws exception: ', data)
})
client.subscribeV5(['order', 'wallet', 'greeks'], 'linear')
client.on('open', function () {
console.log('connection open')
})
client.on('update', function (message) {
console.log('update', message)
})
client.on('response', function (response) {
console.log('response', response)
})
client.on('close', () => {
console.log('connection closed')
})
}
onMount(async () => {
// createPublicWS()
// createPrivateWS()
const response = await fetch('/api/bybit/wallet')
})
const { form: formData, enhance } = form
</script>
<div class="flex flex-1 flex-col items-center justify-center">
<form method="POST" use:enhance class="grid grid-cols-2 gap-4">
<Form.Field {form} name="symbol">
<Form.Control>
{#snippet children({ props })}
<Form.Label>Symbol</Form.Label>
<Input {...props} bind:value={$formData.symbol} />
{/snippet}
</Form.Control>
<Form.Description>Symbol</Form.Description>
<Form.FieldErrors />
</Form.Field>
<Form.Field {form} name="side">
<Form.Control>
{#snippet children({ props })}
<Form.Label>Side</Form.Label>
<Select.Root
type="single"
bind:value={$formData.side}
name={props.name}>
<Select.Trigger {...props}>
{$formData.side ?? 'Select a side'}
</Select.Trigger>
<Select.Content>
<Select.Item value={'Buy'} label={'Buy'} />
<Select.Item value={'Sell'} label={'Sell'} />
</Select.Content>
</Select.Root>
{/snippet}
</Form.Control>
<Form.Description>Side</Form.Description>
<Form.FieldErrors />
</Form.Field>
<Form.Field {form} name="qty">
<Form.Control>
{#snippet children({ props })}
<Form.Label>Quantity (%)</Form.Label>
<Input
type="number"
{...props}
bind:value={$formData.qty} />
{/snippet}
</Form.Control>
<Form.Description>Quantity</Form.Description>
<Form.FieldErrors />
<Slider
type="single"
bind:value={$formData.qty}
max={100}
step={1} />
</Form.Field>
<Form.Field {form} name="takeProfit">
<Form.Control>
{#snippet children({ props })}
<Form.Label>Take Profit (%)</Form.Label>
<Input
type="number"
{...props}
bind:value={$formData.takeProfit} />
{/snippet}
</Form.Control>
<Form.Description>Take Profit</Form.Description>
<Form.FieldErrors />
</Form.Field>
<Form.Field {form} name="stopLoss">
<Form.Control>
{#snippet children({ props })}
<Form.Label>Stop Loss (%)</Form.Label>
<Input
type="number"
{...props}
bind:value={$formData.stopLoss} />
{/snippet}
</Form.Control>
<Form.Description>Stop Loss</Form.Description>
<Form.FieldErrors />
</Form.Field>
<Form.Button>Submit</Form.Button>
</form>
<div>
<div>Webhooks</div>
{#each webhooks as webhook}
<Card.Root>
<Card.Content>
<div>ID: {webhook.id}</div>
<div>Symbol: {webhook.symbol}</div>
<div>Direction: {webhook.direction}</div>
<div>Qty: {webhook.quantityPercent}</div>
<div>Stop Loss (%): {webhook.stopLossPercent}</div>
<div>Take Profit (%): {webhook.takeProfitPercent}</div>
</Card.Content>
</Card.Root>
{/each}
</div>
</div>

@ -0,0 +1,15 @@
import { superValidate } from 'sveltekit-superforms'
import { zod } from 'sveltekit-superforms/adapters'
import type { PageLoad } from './$types'
import { formSchema } from './schema'
export const load = (async () => {
const form = await superValidate(
{
symbol: 'BTCUSDT',
},
zod(formSchema),
{ errors: false },
)
return { form }
}) satisfies PageLoad

@ -0,0 +1,15 @@
import { z } from 'zod'
export type Category = 'spot' | 'linear' | 'inverse'
export const categories: Category[] = ['linear', 'spot', 'inverse']
export const formSchema = z.object({
symbol: z.string(),
side: z.enum(['buy', 'sell']),
qty: z.number().refine((value) => value.toString()),
takeProfit: z.number().refine((value) => value.toString()),
stopLoss: z.number().refine((value) => value.toString()),
})
export type FormSchema = typeof formSchema

@ -0,0 +1,19 @@
export type KLineWSResponse = {
topic: string // "kline.1.BTCUSDT"
data: {
close: string // '97312.42'
confirm: boolean // true
end: number // 1738907339999
high: string // '97325.67'
interval: string // '1'
low: string // '97283.66'
open: string // '97301.99'
start: number // 1738907280000
timestamp: number // 1738907340143
turnover: string // '1231841.67604775'
volume: string // '12.65954'
}[]
ts: number // 1738907340143
type: 'snapshot'
wsKey: 'v5SpotPublic'
}

@ -0,0 +1,315 @@
<script lang="ts">
import * as Form from '$lib/components/ui/form'
import * as Select from '$lib/components/ui/select'
import { Input } from '$lib/components/ui/input'
import { superForm } from 'sveltekit-superforms'
import { zod } from 'sveltekit-superforms/adapters'
import { formSchema, intervals } from './schema'
import { categories } from './schema'
import { CalendarIcon } from 'lucide-svelte'
import { Calendar } from '$lib/components/ui/calendar/index.js'
import * as Popover from '$lib/components/ui/popover/index.js'
import * as Card from '$lib/components/ui/card/index.js'
import * as Tabs from '$lib/components/ui/tabs/index.js'
import {
CalendarDate,
DateFormatter,
type DateValue,
getLocalTimeZone,
parseDate,
today,
} from '@internationalized/date'
import { cn } from '$lib/utils.js'
import { buttonVariants } from '$lib/components/ui/button'
import Bingx from './(components)/bingx.svelte'
import { ScrollArea } from '$lib/components/ui/scroll-area'
let { data } = $props()
const apiEndpoint = 'https://api.bybit.com/v5/market/kline'
const form = superForm(data.form, {
resetForm: false,
SPA: true,
validators: zod(formSchema),
onUpdated: async ({ form }) => {
if (!form.valid) return
console.log(form.data)
// Form validation
const url = new URL(apiEndpoint)
url.searchParams.set('symbol', form.data.symbol)
url.searchParams.set('interval', form.data.interval)
const start = new Date(
form.data.start + ' ' + form.data.startTime + ':00',
)
url.searchParams.set('start', start.getTime().toString())
const end = new Date(
form.data.end + ' ' + form.data.endTime + ':00',
)
url.searchParams.set('end', end.getTime().toString())
if (form.data.limit)
url.searchParams.set('limit', form.data.limit.toString())
if (form.data.category)
url.searchParams.set('category', form.data.category)
const response = await fetch(url)
const data = await response.json()
console.log(data.result.list)
let csvContent =
'data:text/csv;charset=utf-8,' +
'startTime,openPrice,highPrice,lowPrice,closePrice,volume,turnover\n' +
data.result.list
.map((data: string[]) => {
let newData = data
newData[0] = new Date(parseInt(newData[0]))
.toLocaleString()
.replace(',', ' |')
return newData
})
.map((e: any) => e.join(','))
.join('\n')
var encodedUri = encodeURI(csvContent)
var link = document.createElement('a')
link.setAttribute('href', encodedUri)
link.setAttribute('download', 'data.csv')
document.body.appendChild(link)
link.click()
},
})
const { form: formData, enhance } = form
const df = new DateFormatter('en-US', {
dateStyle: 'long',
})
let start = $state<DateValue | undefined>()
let end = $state<DateValue | undefined>()
$effect(() => {
start = $formData.start ? parseDate($formData.start) : undefined
end = $formData.end ? parseDate($formData.end) : undefined
})
let placeholder = $state<DateValue>(today(getLocalTimeZone()))
</script>
<ScrollArea class="max-h-[calc(100vh-64px)] flex-1">
<div class="flex justify-center">
<Card.Root>
<Card.Content>
<Tabs.Root value="bybit" class="w-[400px]">
<Tabs.List>
<Tabs.Trigger value="bybit">ByBit</Tabs.Trigger>
<Tabs.Trigger value="bingx">BingX</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="bybit">
<form method="POST" use:enhance>
<Form.Field {form} name="symbol">
<Form.Control>
{#snippet children({ props })}
<Form.Label>Symbol</Form.Label>
<Input {...props} bind:value={$formData.symbol} />
{/snippet}
</Form.Control>
<Form.Description>Symbol</Form.Description>
<Form.FieldErrors />
</Form.Field>
<Form.Field {form} name="limit">
<Form.Control>
{#snippet children({ props })}
<Form.Label>Limit</Form.Label>
<Input
{...props}
bind:value={$formData.limit}
type="number" />
{/snippet}
</Form.Control>
<Form.Description>Limit</Form.Description>
<Form.FieldErrors />
</Form.Field>
<Form.Field {form} name="category">
<Form.Control>
{#snippet children({ props })}
<Form.Label>Category</Form.Label>
<Select.Root
type="single"
bind:value={$formData.category}
name={props.name}>
<Select.Trigger {...props}>
{$formData.category ?? 'Select a category'}
</Select.Trigger>
<Select.Content>
{#each categories as category}
<Select.Item
value={category}
label={category} />
{/each}
</Select.Content>
</Select.Root>
{/snippet}
</Form.Control>
<Form.Description>Category</Form.Description>
<Form.FieldErrors />
</Form.Field>
<Form.Field {form} name="interval">
<Form.Control>
{#snippet children({ props })}
<Form.Label>Interval</Form.Label>
<Select.Root
type="single"
bind:value={$formData.interval}
name={props.name}>
<Select.Trigger {...props}>
{$formData.interval ?? 'Select a interval'}
</Select.Trigger>
<Select.Content>
{#each intervals as interval}
<Select.Item
value={interval}
label={interval} />
{/each}
</Select.Content>
</Select.Root>
{/snippet}
</Form.Control>
<Form.Description>Interval</Form.Description>
<Form.FieldErrors />
</Form.Field>
<Form.Field {form} name="start" class="flex flex-col">
<Form.Control>
{#snippet children({ props })}
<Form.Label>Start Date</Form.Label>
<Popover.Root>
<Popover.Trigger
{...props}
class={cn(
buttonVariants({ variant: 'outline' }),
'w-[280px] justify-start pl-4 text-left font-normal',
!start && 'text-muted-foreground',
)}>
{start
? df.format(
start.toDate(getLocalTimeZone()),
)
: 'Pick a date'}
<CalendarIcon
class="ml-auto size-4 opacity-50" />
</Popover.Trigger>
<Popover.Content class="w-auto p-0" side="top">
<Calendar
type="single"
value={start as DateValue}
bind:placeholder
minValue={new CalendarDate(1900, 1, 1)}
maxValue={today(getLocalTimeZone())}
calendarLabel="Date of birth"
onValueChange={(v) => {
if (v) {
$formData.start = v.toString()
} else {
$formData.start = ''
}
}} />
</Popover.Content>
</Popover.Root>
<Form.Description>Start Time</Form.Description>
<Form.FieldErrors />
<input
hidden
value={$formData.start}
name={props.name} />
{/snippet}
</Form.Control>
</Form.Field>
<Form.Field {form} name="startTime">
<Form.Control>
{#snippet children({ props })}
<Form.Label>Start Time</Form.Label>
<Input
{...props}
bind:value={$formData.startTime}
type="time" />
{/snippet}
</Form.Control>
<Form.Description>Start Time</Form.Description>
<Form.FieldErrors />
</Form.Field>
<Form.Field {form} name="end" class="flex flex-col">
<Form.Control>
{#snippet children({ props })}
<Form.Label>End Date</Form.Label>
<Popover.Root>
<Popover.Trigger
{...props}
class={cn(
buttonVariants({ variant: 'outline' }),
'w-[280px] justify-start pl-4 text-left font-normal',
!end && 'text-muted-foreground',
)}>
{end
? df.format(end.toDate(getLocalTimeZone()))
: 'Pick a date'}
<CalendarIcon
class="ml-auto size-4 opacity-50" />
</Popover.Trigger>
<Popover.Content class="w-auto p-0" side="top">
<Calendar
type="single"
value={end as DateValue}
bind:placeholder
minValue={new CalendarDate(1900, 1, 1)}
maxValue={today(getLocalTimeZone())}
calendarLabel="Date of birth"
onValueChange={(v) => {
if (v) {
$formData.end = v.toString()
} else {
$formData.end = ''
}
}} />
</Popover.Content>
</Popover.Root>
<Form.Description>End Date</Form.Description>
<Form.FieldErrors />
<input
hidden
value={$formData.end}
name={props.name} />
{/snippet}
</Form.Control>
</Form.Field>
<Form.Field {form} name="endTime">
<Form.Control>
{#snippet children({ props })}
<Form.Label>End Time</Form.Label>
<Input
{...props}
bind:value={$formData.endTime}
type="time" />
{/snippet}
</Form.Control>
<Form.Description>End Time</Form.Description>
<Form.FieldErrors />
</Form.Field>
<Form.Button>Submit</Form.Button>
</form>
</Tabs.Content>
<Tabs.Content value="bingx">
<Bingx />
</Tabs.Content>
</Tabs.Root>
</Card.Content>
</Card.Root>
</div>
</ScrollArea>

@ -0,0 +1,32 @@
<script lang="ts">
const getQuoteByReceivingAmount = async (
fromChain: string,
toChain: string,
fromToken: string,
toToken: string,
toAmount: string,
fromAddress: string,
) => {
const result = await fetch('https://li.quest/v1/quote/toAmount', {
body: JSON.stringify({
params: {
fromChain,
toChain,
fromToken,
toToken,
toAmount,
fromAddress,
},
}),
})
return await result.json()
}
const fromChain = 'DAI'
const fromToken = 'USDC'
const toChain = 'POL'
const toToken = 'USDC'
const toAmount = '1000000' // 1 USDC = 1000000
const fromAddress = YOUR_WALLET_ADDRESS
</script>

@ -0,0 +1,77 @@
<script lang="ts">
import type { Token } from '$lib/tokens'
import { Sifi } from '@sifi/sdk'
import { onMount } from 'svelte'
import * as Card from '$lib/components/ui/card'
const sifi = new Sifi()
let {
fromToken,
toToken,
fromAmount,
fromChain,
}: {
fromToken: Token
toToken: Token
fromAmount: number
fromChain: number
} = $props()
// Get a quote to swap 100 USDC to WETH
let quote = $state(
sifi.getQuote({
fromToken: fromToken.address,
toToken: toToken.address,
fromAmount: BigInt(
fromAmount * Math.pow(10, fromToken.decimal),
),
fromChain: fromChain,
}),
)
$effect(() => {
quote = sifi.getQuote({
fromToken: fromToken.address,
toToken: toToken.address,
fromAmount: BigInt(
fromAmount * Math.pow(10, fromToken.decimal),
),
fromChain: fromChain,
})
})
onMount(() => {
// setInterval(() => {
// }, 10000)
})
</script>
<Card.Root>
<Card.Content>
{#await quote then quote}
<!-- {JSON.stringify(
quote,
(key, value) =>
typeof value === 'bigint' ? value.toString() : value, // return everything else unchanged
)} -->
<div class="flex flex-col gap-2">
<span>
From Amount: {Number(quote.fromAmount) /
Math.pow(10, fromToken.decimal)}
</span>
<span>
To Amount: {Number(quote.toAmount) /
Math.pow(10, toToken.decimal)}
</span>
<span>
Estimated Gas: {quote.estimatedGas}
</span>
<span>
Estimated Gas USD: {quote.source.quote.gasCostUSD}
</span>
</div>
{/await}
</Card.Content>
</Card.Root>

@ -0,0 +1,28 @@
<script lang="ts">
import { onMount } from 'svelte'
const uniswapSubgraphUrl =
'https://gateway.thegraph.com/api/16de87701a9aee18e744b6cc8e7124d5/subgraphs/id/F85MNzUGYqgSHSHRGgeVMNsdnW1KtZSVgFULumXRZTw2'
const query = `
{
pools (orderBy: totalValueLockedETH, orderDirection: desc) {
id
}
}
`
const fetchPoolsData = async () => {
await fetch(uniswapSubgraphUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ query }),
})
}
onMount(() => {
fetchPoolsData()
})
</script>

@ -0,0 +1,163 @@
<script lang="ts">
import * as Avatar from '$lib/components/ui/avatar/index.js'
import * as Card from '$lib/components/ui/card/index.js'
import * as Command from '$lib/components/ui/command/index.js'
import * as Dialog from '$lib/components/ui/dialog/index.js'
import { Input } from '$lib/components/ui/input/index.js'
import { Button } from '$lib/components/ui/button/index.js'
import Sifi from './(components)/sifi.svelte'
import { ArrowDownUpIcon } from 'lucide-svelte'
import { tokens, type Token } from '$lib/tokens'
import Uniswap from './(components)/uniswap.svelte'
let { data } = $props()
let changeTokenDialogOpen = $state(false)
let changeTokenMode = $state<'from' | 'to'>('from')
let fromToken = $state<Token>(tokens['56'][0])
let fromAmount = $state('')
let toToken = $state<Token>(tokens[56][1])
let toAmount = $state('')
const regexp = /^\d*\.?\d*$/
const swapInput = () => {
const tempToken = fromToken
const tempAmount = fromAmount
fromToken = toToken
fromAmount = toAmount
toToken = tempToken
toAmount = tempAmount
}
</script>
<div
class="flex flex-1 flex-col items-center justify-center gap-4 p-4">
<!-- <Sifi /> -->
<Uniswap />
<Card.Root class="w-full max-w-[500px]">
<Card.Content>
<div class="flex flex-col gap-4">
<p>From</p>
<div class="flex gap-4">
<Input
type="text"
placeholder="0.0"
bind:value={() => fromAmount,
(value) => {
const valid = regexp.test(value)
if (valid) {
fromAmount = value
}
}} />
<Button
variant="outline"
onclick={() => {
changeTokenMode = 'from'
changeTokenDialogOpen = true
}}>
<Avatar.Root class="size-6">
<Avatar.Image
src={`/${fromToken.icon}`}
alt={fromToken.icon} />
<Avatar.Fallback>{fromToken.symbol}</Avatar.Fallback>
</Avatar.Root>
{fromToken.symbol}
</Button>
</div>
</div>
</Card.Content>
</Card.Root>
<Button
variant="outline"
size="icon"
class="rounded-full"
onclick={swapInput}>
<ArrowDownUpIcon />
</Button>
<Card.Root class="w-full max-w-[500px]">
<Card.Content>
<div class="flex flex-col gap-4">
<p>To</p>
<div class="flex gap-4">
<Input
disabled
placeholder="0.0"
bind:value={() => toAmount,
(value) => {
const valid = regexp.test(value)
if (valid) {
toAmount = value
}
}} />
<Button
variant="outline"
onclick={() => {
changeTokenMode = 'to'
changeTokenDialogOpen = true
}}>
<Avatar.Root class="size-6">
<Avatar.Image
src={`/${toToken.icon}`}
alt={toToken.icon} />
<Avatar.Fallback>{toToken.symbol}</Avatar.Fallback>
</Avatar.Root>
{toToken.symbol}
</Button>
</div>
</div>
</Card.Content>
</Card.Root>
{#if parseFloat(fromAmount) > 0 && fromToken && toToken}
<div class="flex flex-wrap gap-4">
<Sifi
fromAmount={parseFloat(fromAmount)}
fromChain={56}
{fromToken}
{toToken} />
</div>
{/if}
</div>
<Dialog.Root bind:open={changeTokenDialogOpen}>
<Dialog.Content>
<Command.Root>
<Dialog.Header>
<Command.Input placeholder="Search A Token" />
</Dialog.Header>
<Command.List>
{#each tokens['56'] as token}
<Command.Item
onclick={() => {
if (changeTokenMode === 'from') {
const temp = token
if (toToken.symbol === token.symbol) {
toToken = fromToken
}
fromToken = temp
}
if (changeTokenMode === 'to') {
const temp = token
if (fromToken.symbol === token.symbol) {
fromToken = toToken
}
toToken = temp
}
changeTokenDialogOpen = false
}}>
<Avatar.Root class="size-6">
<Avatar.Image src={`/${token.icon}`} alt={token.icon} />
<Avatar.Fallback>{token.symbol}</Avatar.Fallback>
</Avatar.Root>
{token.name} ({token.symbol})
</Command.Item>
{/each}
</Command.List>
</Command.Root>
</Dialog.Content>
</Dialog.Root>

@ -0,0 +1,143 @@
import { hash, verify } from '@node-rs/argon2'
import { encodeBase32LowerCase } from '@oslojs/encoding'
import { fail, redirect } from '@sveltejs/kit'
import { eq } from 'drizzle-orm'
import * as auth from '$lib/server/auth'
import { db } from '$lib/db'
import * as table from '$lib/db/schema'
import type { Actions, PageServerLoad } from './$types'
import { z } from 'zod'
export const load: PageServerLoad = async (event) => {
if (event.locals.user) {
return redirect(302, '/dashboard')
}
return {}
}
export const actions: Actions = {
login: async (event) => {
const formData = await event.request.formData()
const email = formData.get('email')
const password = formData.get('password')
const form = z
.object({
email: z.string().email(),
password: z.string(),
})
.safeParse({ email: email, password })
if (!form.success) {
return fail(400, { message: 'Invalid Credentials' })
}
const results = await db
.select()
.from(table.user)
.where(eq(table.user.email, form.data.email))
const existingUser = results.at(0)
if (!existingUser) {
return fail(400, { message: 'Incorrect username or password' })
}
const validPassword = await Bun.password.verify(
form.data.password,
existingUser.passwordHash,
)
if (!validPassword) {
return fail(400, { message: 'Incorrect username or password' })
}
const sessionToken = auth.generateSessionToken()
const session = await auth.createSession(
sessionToken,
existingUser.id,
)
auth.setSessionTokenCookie(
event,
sessionToken,
new Date(session.expiresAt),
)
return redirect(302, '/dashboard')
},
register: async (event) => {
const formData = await event.request.formData()
const email = formData.get('email')
const password = formData.get('password')
const form = z
.object({
email: z.string().email(),
password: z.string(),
})
.safeParse({ email: email, password })
if (!form.success) {
return fail(400, { message: 'Invalid Credentials' })
}
const userId = generateUserId()
const passwordHash = await Bun.password.hash(form.data.password)
try {
const user = await db.query.user.findFirst({
where: (user, { eq }) => eq(user.email, form.data.email),
})
if (user) {
const validPassword = await Bun.password.verify(
form.data.password,
passwordHash,
)
if (!validPassword) {
return fail(400, {
message: 'Incorrect username or password',
})
}
const sessionToken = auth.generateSessionToken()
const session = await auth.createSession(
sessionToken,
user.id,
)
auth.setSessionTokenCookie(
event,
sessionToken,
new Date(session.expiresAt),
)
return redirect(302, '/dashboard')
}
await db.insert(table.user).values({
id: userId,
email: form.data.email,
passwordHash,
createdAt: Date.now(),
})
const sessionToken = auth.generateSessionToken()
const session = await auth.createSession(sessionToken, userId)
auth.setSessionTokenCookie(
event,
sessionToken,
new Date(session.expiresAt),
)
} catch (e) {
console.log(e)
return fail(500, { message: 'An error has occurred' })
}
return redirect(302, '/dashboard')
},
}
function generateUserId() {
// ID with 120 bits of entropy, or about the same as UUID v4.
const bytes = crypto.getRandomValues(new Uint8Array(15))
const id = encodeBase32LowerCase(bytes)
return id
}

@ -0,0 +1,21 @@
<script lang="ts">
import { enhance } from '$app/forms'
import type { ActionData } from './$types'
let { form }: { form: ActionData } = $props()
</script>
<h1>Login/Register</h1>
<form method="post" action="?/login" use:enhance>
<label>
Email
<input name="email" />
</label>
<label>
Password
<input type="password" name="password" />
</label>
<button>Login</button>
<button formaction="?/register">Register</button>
</form>
<p style="color: red">{form?.message ?? ''}</p>

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save