improve home page & account footer
This commit is contained in:
parent
af078e7ba2
commit
6b2d0f5cbc
23 changed files with 551 additions and 36 deletions
|
|
@ -1,23 +1,29 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import * as Sidebar from '$lib/components/ui/sidebar';
|
import * as Sidebar from '$lib/components/ui/sidebar';
|
||||||
|
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
||||||
|
import * as Avatar from '$lib/components/ui/avatar';
|
||||||
import {
|
import {
|
||||||
Moon,
|
Moon,
|
||||||
Sun,
|
Sun,
|
||||||
PenSquare,
|
|
||||||
ShieldAlert,
|
ShieldAlert,
|
||||||
Home,
|
Home,
|
||||||
Store,
|
Store,
|
||||||
BriefcaseBusiness,
|
BriefcaseBusiness,
|
||||||
Coins
|
Coins,
|
||||||
|
ChevronsUpDownIcon,
|
||||||
|
SparklesIcon,
|
||||||
|
BadgeCheckIcon,
|
||||||
|
CreditCardIcon,
|
||||||
|
BellIcon,
|
||||||
|
LogOutIcon
|
||||||
} from 'lucide-svelte';
|
} from 'lucide-svelte';
|
||||||
import { mode, setMode } from 'mode-watcher';
|
import { mode, setMode } from 'mode-watcher';
|
||||||
import type { HTMLAttributes } from 'svelte/elements';
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
import Button from '../ui/button/button.svelte';
|
|
||||||
import { USER_DATA } from '$lib/stores/user-data';
|
import { USER_DATA } from '$lib/stores/user-data';
|
||||||
import { useSidebar } from '$lib/components/ui/sidebar/index.js';
|
import { useSidebar } from '$lib/components/ui/sidebar/index.js';
|
||||||
import { signIn } from '$lib/auth-client';
|
|
||||||
import { page } from '$app/state';
|
|
||||||
import SignInConfirmDialog from './SignInConfirmDialog.svelte';
|
import SignInConfirmDialog from './SignInConfirmDialog.svelte';
|
||||||
|
import { signOut } from '$lib/auth-client';
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
navMain: [
|
navMain: [
|
||||||
|
|
@ -30,28 +36,20 @@
|
||||||
};
|
};
|
||||||
type MenuButtonProps = HTMLAttributes<HTMLAnchorElement | HTMLButtonElement>;
|
type MenuButtonProps = HTMLAttributes<HTMLAnchorElement | HTMLButtonElement>;
|
||||||
|
|
||||||
const { setOpenMobile } = useSidebar();
|
const { setOpenMobile, isMobile } = useSidebar();
|
||||||
let shouldSignIn = $state(false);
|
let shouldSignIn = $state(false);
|
||||||
|
|
||||||
function handleNavClick(title: string) {
|
function handleNavClick(title: string) {
|
||||||
setOpenMobile(false);
|
setOpenMobile(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleSignIn() {
|
|
||||||
console.log("callbackL ", `${page.url.pathname}?signIn=1`);
|
|
||||||
await signIn.social({
|
|
||||||
provider: 'google',
|
|
||||||
callbackURL: `${page.url.pathname}?signIn=1`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleModeToggle() {
|
function handleModeToggle() {
|
||||||
setMode(mode.current === 'light' ? 'dark' : 'light');
|
setMode(mode.current === 'light' ? 'dark' : 'light');
|
||||||
setOpenMobile(false);
|
setOpenMobile(false);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SignInConfirmDialog bind:open={shouldSignIn} onConfirm={handleSignIn} />
|
<SignInConfirmDialog bind:open={shouldSignIn} />
|
||||||
<Sidebar.Root collapsible="offcanvas">
|
<Sidebar.Root collapsible="offcanvas">
|
||||||
<Sidebar.Header>
|
<Sidebar.Header>
|
||||||
<div class="flex items-center gap-1 px-2 py-2">
|
<div class="flex items-center gap-1 px-2 py-2">
|
||||||
|
|
@ -120,21 +118,92 @@
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</Sidebar.MenuButton>
|
</Sidebar.MenuButton>
|
||||||
</Sidebar.MenuItem>
|
</Sidebar.MenuItem>
|
||||||
|
|
||||||
{#if !$USER_DATA}
|
|
||||||
<Sidebar.MenuItem>
|
|
||||||
<Sidebar.MenuButton>
|
|
||||||
{#snippet child()}
|
|
||||||
<Button onclick={() => shouldSignIn = !shouldSignIn} class={`mb-4 h-[3.0rem] w-36 cursor-pointer`}>
|
|
||||||
<PenSquare />
|
|
||||||
<span>Sign in</span>
|
|
||||||
</Button>
|
|
||||||
{/snippet}
|
|
||||||
</Sidebar.MenuButton>
|
|
||||||
</Sidebar.MenuItem>
|
|
||||||
{/if}
|
|
||||||
</Sidebar.Menu>
|
</Sidebar.Menu>
|
||||||
</Sidebar.GroupContent>
|
</Sidebar.GroupContent>
|
||||||
</Sidebar.Group>
|
</Sidebar.Group>
|
||||||
</Sidebar.Content>
|
</Sidebar.Content>
|
||||||
|
|
||||||
|
{#if $USER_DATA}
|
||||||
|
<Sidebar.Footer>
|
||||||
|
<Sidebar.Menu>
|
||||||
|
<Sidebar.MenuItem>
|
||||||
|
<DropdownMenu.Root>
|
||||||
|
<DropdownMenu.Trigger>
|
||||||
|
{#snippet child({ props })}
|
||||||
|
<Sidebar.MenuButton
|
||||||
|
size="lg"
|
||||||
|
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<Avatar.Root class="size-8 rounded-lg">
|
||||||
|
<Avatar.Image src={$USER_DATA.image} alt={$USER_DATA.name} />
|
||||||
|
<Avatar.Fallback class="rounded-lg">CN</Avatar.Fallback>
|
||||||
|
</Avatar.Root>
|
||||||
|
<div class="grid flex-1 text-left text-sm leading-tight">
|
||||||
|
<span class="truncate font-medium">{$USER_DATA.name}</span>
|
||||||
|
<span class="truncate text-xs">$35,674.34</span>
|
||||||
|
<!-- TODO: replace with actual db entry -->
|
||||||
|
</div>
|
||||||
|
<ChevronsUpDownIcon class="ml-auto size-4" />
|
||||||
|
</Sidebar.MenuButton>
|
||||||
|
{/snippet}
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
<DropdownMenu.Content
|
||||||
|
class="w-(--bits-dropdown-menu-anchor-width) min-w-56 rounded-lg"
|
||||||
|
side={isMobile ? 'bottom' : 'right'}
|
||||||
|
align="end"
|
||||||
|
sideOffset={4}
|
||||||
|
>
|
||||||
|
<DropdownMenu.Label class="p-0 font-normal">
|
||||||
|
<div class="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
||||||
|
<Avatar.Root class="size-8 rounded-lg">
|
||||||
|
<Avatar.Image src={$USER_DATA.image} alt={$USER_DATA.name} />
|
||||||
|
<Avatar.Fallback class="rounded-lg">CN</Avatar.Fallback>
|
||||||
|
</Avatar.Root>
|
||||||
|
<div class="grid flex-1 text-left text-sm leading-tight">
|
||||||
|
<span class="truncate font-medium">{$USER_DATA.name}</span>
|
||||||
|
<span class="truncate text-xs">{$USER_DATA.email}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DropdownMenu.Label>
|
||||||
|
<DropdownMenu.Separator />
|
||||||
|
<DropdownMenu.Group>
|
||||||
|
<DropdownMenu.Item disabled={true}>
|
||||||
|
<SparklesIcon />
|
||||||
|
Upgrade to Pro
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</DropdownMenu.Group>
|
||||||
|
<DropdownMenu.Separator />
|
||||||
|
<DropdownMenu.Group>
|
||||||
|
<DropdownMenu.Item>
|
||||||
|
<BadgeCheckIcon />
|
||||||
|
Account
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item disabled={true}>
|
||||||
|
<CreditCardIcon />
|
||||||
|
Billing
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item disabled={true}>
|
||||||
|
<BellIcon />
|
||||||
|
Notifications
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</DropdownMenu.Group>
|
||||||
|
<DropdownMenu.Separator />
|
||||||
|
<DropdownMenu.Item
|
||||||
|
onclick={() => {
|
||||||
|
signOut().then(() => {
|
||||||
|
USER_DATA.set(null);
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<LogOutIcon />
|
||||||
|
Log out
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Root>
|
||||||
|
</Sidebar.MenuItem>
|
||||||
|
</Sidebar.Menu>
|
||||||
|
</Sidebar.Footer>
|
||||||
|
{/if}
|
||||||
</Sidebar.Root>
|
</Sidebar.Root>
|
||||||
|
|
|
||||||
|
|
@ -7,17 +7,25 @@
|
||||||
DialogDescription
|
DialogDescription
|
||||||
} from '$lib/components/ui/dialog';
|
} from '$lib/components/ui/dialog';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
|
import { signIn } from '$lib/auth-client';
|
||||||
|
import { page } from '$app/state';
|
||||||
|
|
||||||
let { open = $bindable(false), onConfirm } = $props<{
|
async function onConfirm() {
|
||||||
|
await signIn.social({
|
||||||
|
provider: 'google',
|
||||||
|
callbackURL: `${page.url.pathname}?signIn=1`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let { open = $bindable(false) } = $props<{
|
||||||
open?: boolean;
|
open?: boolean;
|
||||||
onConfirm: (provider: 'google') => void;
|
|
||||||
}>();
|
}>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Dialog bind:open>
|
<Dialog bind:open>
|
||||||
<DialogContent class="sm:max-w-md">
|
<DialogContent class="sm:max-w-md">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Sign in to Vyntr</DialogTitle>
|
<DialogTitle>Sign in to Rugplay</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
Choose a service to sign in with. Your account will be created automatically if you don't
|
Choose a service to sign in with. Your account will be created automatically if you don't
|
||||||
have one.
|
have one.
|
||||||
|
|
@ -27,7 +35,7 @@
|
||||||
<Button
|
<Button
|
||||||
class="flex w-full items-center justify-center gap-2"
|
class="flex w-full items-center justify-center gap-2"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onclick={() => onConfirm('google')}
|
onclick={() => onConfirm()}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
class="h-5 w-5"
|
class="h-5 w-5"
|
||||||
|
|
|
||||||
17
website/src/lib/components/ui/avatar/avatar-fallback.svelte
Normal file
17
website/src/lib/components/ui/avatar/avatar-fallback.svelte
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
<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
|
||||||
|
data-slot="avatar-fallback"
|
||||||
|
class={cn("bg-muted flex size-full items-center justify-center rounded-full", className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
17
website/src/lib/components/ui/avatar/avatar-image.svelte
Normal file
17
website/src/lib/components/ui/avatar/avatar-image.svelte
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
<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
|
||||||
|
data-slot="avatar-image"
|
||||||
|
class={cn("aspect-square size-full", className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
17
website/src/lib/components/ui/avatar/avatar.svelte
Normal file
17
website/src/lib/components/ui/avatar/avatar.svelte
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
<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
|
||||||
|
data-slot="avatar"
|
||||||
|
class={cn("relative flex size-8 shrink-0 overflow-hidden rounded-full", className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
13
website/src/lib/components/ui/avatar/index.ts
Normal file
13
website/src/lib/components/ui/avatar/index.ts
Normal file
|
|
@ -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,
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||||
|
import CheckIcon from "@lucide/svelte/icons/check";
|
||||||
|
import MinusIcon from "@lucide/svelte/icons/minus";
|
||||||
|
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||||
|
import type { Snippet } from "svelte";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
checked = $bindable(false),
|
||||||
|
indeterminate = $bindable(false),
|
||||||
|
class: className,
|
||||||
|
children: childrenProp,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChildrenOrChild<DropdownMenuPrimitive.CheckboxItemProps> & {
|
||||||
|
children?: Snippet;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.CheckboxItem
|
||||||
|
bind:ref
|
||||||
|
bind:checked
|
||||||
|
bind:indeterminate
|
||||||
|
data-slot="dropdown-menu-checkbox-item"
|
||||||
|
class={cn(
|
||||||
|
"focus:bg-accent focus:text-accent-foreground outline-hidden relative flex cursor-default select-none items-center gap-2 rounded-sm py-1.5 pl-8 pr-2 text-sm data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{#snippet children({ checked, indeterminate })}
|
||||||
|
<span class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||||
|
{#if indeterminate}
|
||||||
|
<MinusIcon class="size-4" />
|
||||||
|
{:else}
|
||||||
|
<CheckIcon class={cn("size-4", !checked && "text-transparent")} />
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
{@render childrenProp?.()}
|
||||||
|
{/snippet}
|
||||||
|
</DropdownMenuPrimitive.CheckboxItem>
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
sideOffset = 4,
|
||||||
|
portalProps,
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: DropdownMenuPrimitive.ContentProps & {
|
||||||
|
portalProps?: DropdownMenuPrimitive.PortalProps;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.Portal {...portalProps}>
|
||||||
|
<DropdownMenuPrimitive.Content
|
||||||
|
bind:ref
|
||||||
|
data-slot="dropdown-menu-content"
|
||||||
|
{sideOffset}
|
||||||
|
class={cn(
|
||||||
|
"bg-popover text-popover-foreground 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-[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 max-h-(--radix-dropdown-menu-content-available-height) origin-(--radix-dropdown-menu-content-transform-origin) z-50 min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border p-1 shadow-md",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
|
</DropdownMenuPrimitive.Portal>
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
import type { ComponentProps } from "svelte";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
inset,
|
||||||
|
...restProps
|
||||||
|
}: ComponentProps<typeof DropdownMenuPrimitive.GroupHeading> & {
|
||||||
|
inset?: boolean;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.GroupHeading
|
||||||
|
bind:ref
|
||||||
|
data-slot="dropdown-menu-group-heading"
|
||||||
|
data-inset={inset}
|
||||||
|
class={cn("px-2 py-1.5 text-sm font-semibold data-[inset]:pl-8", className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||||
|
|
||||||
|
let { ref = $bindable(null), ...restProps }: DropdownMenuPrimitive.GroupProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.Group bind:ref data-slot="dropdown-menu-group" {...restProps} />
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
inset,
|
||||||
|
variant = "default",
|
||||||
|
...restProps
|
||||||
|
}: DropdownMenuPrimitive.ItemProps & {
|
||||||
|
inset?: boolean;
|
||||||
|
variant?: "default" | "destructive";
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.Item
|
||||||
|
bind:ref
|
||||||
|
data-slot="dropdown-menu-item"
|
||||||
|
data-inset={inset}
|
||||||
|
data-variant={variant}
|
||||||
|
class={cn(
|
||||||
|
"data-highlighted:bg-accent data-highlighted:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:data-highlighted:bg-destructive/10 dark:data-[variant=destructive]:data-highlighted:bg-destructive/20 data-[variant=destructive]:data-highlighted:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground outline-hidden relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm data-[disabled]:pointer-events-none data-[inset]:pl-8 data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
inset,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
||||||
|
inset?: boolean;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="dropdown-menu-label"
|
||||||
|
data-inset={inset}
|
||||||
|
class={cn("px-2 py-1.5 text-sm font-semibold data-[inset]:pl-8", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
value = $bindable(),
|
||||||
|
...restProps
|
||||||
|
}: DropdownMenuPrimitive.RadioGroupProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.RadioGroup
|
||||||
|
bind:ref
|
||||||
|
bind:value
|
||||||
|
data-slot="dropdown-menu-radio-group"
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||||
|
import CircleIcon from "@lucide/svelte/icons/circle";
|
||||||
|
import { cn, type WithoutChild } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children: childrenProp,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChild<DropdownMenuPrimitive.RadioItemProps> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.RadioItem
|
||||||
|
bind:ref
|
||||||
|
data-slot="dropdown-menu-radio-item"
|
||||||
|
class={cn(
|
||||||
|
"focus:bg-accent focus:text-accent-foreground outline-hidden relative flex cursor-default select-none items-center gap-2 rounded-sm py-1.5 pl-8 pr-2 text-sm data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{#snippet children({ checked })}
|
||||||
|
<span class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||||
|
{#if checked}
|
||||||
|
<CircleIcon class="size-2 fill-current" />
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
{@render childrenProp?.({ checked })}
|
||||||
|
{/snippet}
|
||||||
|
</DropdownMenuPrimitive.RadioItem>
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: DropdownMenuPrimitive.SeparatorProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.Separator
|
||||||
|
bind:ref
|
||||||
|
data-slot="dropdown-menu-separator"
|
||||||
|
class={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLSpanElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<span
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="dropdown-menu-shortcut"
|
||||||
|
class={cn("text-muted-foreground ml-auto text-xs tracking-widest", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</span>
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: DropdownMenuPrimitive.SubContentProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.SubContent
|
||||||
|
bind:ref
|
||||||
|
data-slot="dropdown-menu-sub-content"
|
||||||
|
class={cn(
|
||||||
|
"bg-popover text-popover-foreground 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-[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 origin-(--radix-dropdown-menu-content-transform-origin) z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-lg",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||||
|
import ChevronRightIcon from "@lucide/svelte/icons/chevron-right";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
inset,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: DropdownMenuPrimitive.SubTriggerProps & {
|
||||||
|
inset?: boolean;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.SubTrigger
|
||||||
|
bind:ref
|
||||||
|
data-slot="dropdown-menu-sub-trigger"
|
||||||
|
data-inset={inset}
|
||||||
|
class={cn(
|
||||||
|
"data-highlighted:bg-accent data-highlighted:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground outline-hidden [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm data-[disabled]:pointer-events-none data-[inset]:pl-8 data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
<ChevronRightIcon class="ml-auto size-4" />
|
||||||
|
</DropdownMenuPrimitive.SubTrigger>
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||||
|
|
||||||
|
let { ref = $bindable(null), ...restProps }: DropdownMenuPrimitive.TriggerProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.Trigger bind:ref data-slot="dropdown-menu-trigger" {...restProps} />
|
||||||
49
website/src/lib/components/ui/dropdown-menu/index.ts
Normal file
49
website/src/lib/components/ui/dropdown-menu/index.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||||
|
import CheckboxItem from "./dropdown-menu-checkbox-item.svelte";
|
||||||
|
import Content from "./dropdown-menu-content.svelte";
|
||||||
|
import Group from "./dropdown-menu-group.svelte";
|
||||||
|
import Item from "./dropdown-menu-item.svelte";
|
||||||
|
import Label from "./dropdown-menu-label.svelte";
|
||||||
|
import RadioGroup from "./dropdown-menu-radio-group.svelte";
|
||||||
|
import RadioItem from "./dropdown-menu-radio-item.svelte";
|
||||||
|
import Separator from "./dropdown-menu-separator.svelte";
|
||||||
|
import Shortcut from "./dropdown-menu-shortcut.svelte";
|
||||||
|
import Trigger from "./dropdown-menu-trigger.svelte";
|
||||||
|
import SubContent from "./dropdown-menu-sub-content.svelte";
|
||||||
|
import SubTrigger from "./dropdown-menu-sub-trigger.svelte";
|
||||||
|
import GroupHeading from "./dropdown-menu-group-heading.svelte";
|
||||||
|
const Sub = DropdownMenuPrimitive.Sub;
|
||||||
|
const Root = DropdownMenuPrimitive.Root;
|
||||||
|
|
||||||
|
export {
|
||||||
|
CheckboxItem,
|
||||||
|
Content,
|
||||||
|
Root as DropdownMenu,
|
||||||
|
CheckboxItem as DropdownMenuCheckboxItem,
|
||||||
|
Content as DropdownMenuContent,
|
||||||
|
Group as DropdownMenuGroup,
|
||||||
|
Item as DropdownMenuItem,
|
||||||
|
Label as DropdownMenuLabel,
|
||||||
|
RadioGroup as DropdownMenuRadioGroup,
|
||||||
|
RadioItem as DropdownMenuRadioItem,
|
||||||
|
Separator as DropdownMenuSeparator,
|
||||||
|
Shortcut as DropdownMenuShortcut,
|
||||||
|
Sub as DropdownMenuSub,
|
||||||
|
SubContent as DropdownMenuSubContent,
|
||||||
|
SubTrigger as DropdownMenuSubTrigger,
|
||||||
|
Trigger as DropdownMenuTrigger,
|
||||||
|
GroupHeading as DropdownMenuGroupHeading,
|
||||||
|
Group,
|
||||||
|
GroupHeading,
|
||||||
|
Item,
|
||||||
|
Label,
|
||||||
|
RadioGroup,
|
||||||
|
RadioItem,
|
||||||
|
Root,
|
||||||
|
Separator,
|
||||||
|
Shortcut,
|
||||||
|
Sub,
|
||||||
|
SubContent,
|
||||||
|
SubTrigger,
|
||||||
|
Trigger,
|
||||||
|
};
|
||||||
|
|
@ -11,3 +11,17 @@ export type WithoutChild<T> = T extends { child?: any } ? Omit<T, "child"> : T;
|
||||||
export type WithoutChildren<T> = T extends { children?: any } ? Omit<T, "children"> : T;
|
export type WithoutChildren<T> = T extends { children?: any } ? Omit<T, "children"> : T;
|
||||||
export type WithoutChildrenOrChild<T> = WithoutChildren<WithoutChild<T>>;
|
export type WithoutChildrenOrChild<T> = WithoutChildren<WithoutChild<T>>;
|
||||||
export type WithElementRef<T, U extends HTMLElement = HTMLElement> = T & { ref?: U | null };
|
export type WithElementRef<T, U extends HTMLElement = HTMLElement> = T & { ref?: U | null };
|
||||||
|
|
||||||
|
export function getTimeBasedGreeting(name: string): string {
|
||||||
|
const hour = new Date().getHours();
|
||||||
|
|
||||||
|
if (hour < 12) {
|
||||||
|
return `Good morning, ${name}`;
|
||||||
|
} else if (hour < 17) {
|
||||||
|
return `Good afternoon, ${name}`;
|
||||||
|
} else if (hour < 22) {
|
||||||
|
return `Good evening, ${name}`;
|
||||||
|
} else {
|
||||||
|
return `Good night, ${name}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -60,8 +60,8 @@
|
||||||
);
|
);
|
||||||
|
|
||||||
const url = new URL(window.location.href);
|
const url = new URL(window.location.href);
|
||||||
if (url.searchParams.has('signedIn')) {
|
if (url.searchParams.has('signIn')) {
|
||||||
url.searchParams.delete('signedIn');
|
url.searchParams.delete('signIn');
|
||||||
window.history.replaceState({}, '', url);
|
window.history.replaceState({}, '', url);
|
||||||
invalidateAll();
|
invalidateAll();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,35 @@
|
||||||
import * as Card from '$lib/components/ui/card';
|
import * as Card from '$lib/components/ui/card';
|
||||||
import * as Table from '$lib/components/ui/table';
|
import * as Table from '$lib/components/ui/table';
|
||||||
import { Badge } from '$lib/components/ui/badge';
|
import { Badge } from '$lib/components/ui/badge';
|
||||||
|
import { getTimeBasedGreeting } from '$lib/utils';
|
||||||
|
import { USER_DATA } from '$lib/stores/user-data';
|
||||||
|
import SignInConfirmDialog from '$lib/components/self/SignInConfirmDialog.svelte';
|
||||||
|
|
||||||
|
let shouldSignIn = $state(false);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<SignInConfirmDialog bind:open={shouldSignIn} />
|
||||||
|
|
||||||
<div class="container mx-auto p-6">
|
<div class="container mx-auto p-6">
|
||||||
<header class="mb-8">
|
<header class="mb-8">
|
||||||
<h1 class="mb-2 text-4xl font-bold">Rugplay</h1>
|
<h1 class="mb-2 text-3xl font-bold">
|
||||||
<p class="text-muted-foreground">A trading simulator</p>
|
{$USER_DATA ? getTimeBasedGreeting($USER_DATA?.name) : 'Welcome to Rugplay!'}
|
||||||
|
</h1>
|
||||||
|
<p class="text-muted-foreground">
|
||||||
|
{#if $USER_DATA}
|
||||||
|
Here's the market overview for today.
|
||||||
|
{:else}
|
||||||
|
You need to <button
|
||||||
|
class="text-primary underline hover:cursor-pointer"
|
||||||
|
onclick={() => (shouldSignIn = !shouldSignIn)}>sign in</button
|
||||||
|
>
|
||||||
|
or{' '}
|
||||||
|
<button
|
||||||
|
class="text-primary underline hover:cursor-pointer"
|
||||||
|
onclick={() => (shouldSignIn = !shouldSignIn)}>create an account</button
|
||||||
|
> to play.
|
||||||
|
{/if}
|
||||||
|
</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
|
<div class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||||
|
|
|
||||||
Reference in a new issue