Implement most of status system (still kinda bootycheeks at checking if
you're connected), fionally fix prettierrc
This commit is contained in:
parent
126acf52f3
commit
e64910d895
29 changed files with 2001 additions and 879 deletions
|
|
@ -1,22 +1,27 @@
|
|||
<script lang="ts">
|
||||
import { type Data, type OverviewUser } from "$lib";
|
||||
import * as Collapsible from "$lib/components/ui/collapsible/index.js";
|
||||
import * as Sidebar from "$lib/components/ui/sidebar/index.js";
|
||||
import * as Dialog from "$lib/components/ui/dialog/index.js";
|
||||
import * as Tabs from "$lib/components/ui/tabs/index.js";
|
||||
import * as Card from "$lib/components/ui/card/index.js";
|
||||
import MessagesSquare from "@lucide/svelte/icons/messages-square";
|
||||
import MinusIcon from "@lucide/svelte/icons/minus";
|
||||
import PlusIcon from "@lucide/svelte/icons/plus";
|
||||
import { type Data, type OverviewUser } from '$lib';
|
||||
import * as Collapsible from '$lib/components/ui/collapsible/index.js';
|
||||
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
|
||||
import * as Dialog from '$lib/components/ui/dialog/index.js';
|
||||
import * as Tabs from '$lib/components/ui/tabs/index.js';
|
||||
import * as Card from '$lib/components/ui/card/index.js';
|
||||
import MessagesSquare from '@lucide/svelte/icons/messages-square';
|
||||
import MinusIcon from '@lucide/svelte/icons/minus';
|
||||
import PlusIcon from '@lucide/svelte/icons/plus';
|
||||
import UserRoundPlus from '@lucide/svelte/icons/user-round-plus';
|
||||
import UsersRound from '@lucide/svelte/icons/users-round';
|
||||
import CirclePlus from '@lucide/svelte/icons/circle-plus';
|
||||
import Input from "./ui/input/input.svelte";
|
||||
import Button, { buttonVariants } from "./ui/button/button.svelte";
|
||||
import User from "./extra/User.svelte";
|
||||
import type { SessionValidationResult } from "$lib/server/auth";
|
||||
import Input from './ui/input/input.svelte';
|
||||
import Button, { buttonVariants } from './ui/button/button.svelte';
|
||||
import User from './extra/User.svelte';
|
||||
import type { SessionValidationResult } from '$lib/server/auth';
|
||||
|
||||
let { currentPage = $bindable<string|null>(), data, user, ...restProps }: {currentPage: string|null, data: Data, user: SessionValidationResult['user'] }= $props();
|
||||
let {
|
||||
currentPage = $bindable<string | null>(),
|
||||
data,
|
||||
user,
|
||||
...restProps
|
||||
}: { currentPage: string | null; data: Data; user: SessionValidationResult['user'] } = $props();
|
||||
</script>
|
||||
|
||||
<Sidebar.Root {...restProps}>
|
||||
|
|
@ -25,7 +30,7 @@
|
|||
<Sidebar.MenuItem>
|
||||
<Sidebar.MenuButton size="lg">
|
||||
<div
|
||||
class="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg"
|
||||
class="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground"
|
||||
>
|
||||
<MessagesSquare class="size-4" />
|
||||
</div>
|
||||
|
|
@ -33,193 +38,170 @@
|
|||
<span class="font-medium">chat.sad.ovh</span>
|
||||
</div>
|
||||
</Sidebar.MenuButton>
|
||||
<div class="w-full flex gap-2 justify-center">
|
||||
<div class="flex w-full justify-center gap-2">
|
||||
<Dialog.Root>
|
||||
<Dialog.Trigger>
|
||||
<Button
|
||||
variant={user!.friendRequests.length > 0 ? 'destructive' : 'outline'}
|
||||
size="icon"
|
||||
>
|
||||
<UserRoundPlus />
|
||||
</Button>
|
||||
</Dialog.Trigger>
|
||||
|
||||
<Dialog.Root>
|
||||
<Dialog.Trigger>
|
||||
<Button variant={user!.friendRequests.length > 0 ? "destructive" : "outline"} size="icon">
|
||||
<UserRoundPlus />
|
||||
<Dialog.Content class="sm:max-w-[425px]">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Add a friend</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
Add a friend using their username or manage pending requests.
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
|
||||
</Button>
|
||||
</Dialog.Trigger>
|
||||
<!-- input to add a new friend -->
|
||||
<form method="POST" action="?/addFriend" class="mb-4">
|
||||
<Input name="username" placeholder="username" required class="mb-2" />
|
||||
<Dialog.Footer>
|
||||
<Dialog.Close class={buttonVariants({ variant: 'outline' })}>Cancel</Dialog.Close>
|
||||
<Button type="submit">Send request</Button>
|
||||
</Dialog.Footer>
|
||||
</form>
|
||||
|
||||
<!-- Tabs for Friend Requests -->
|
||||
<Tabs.Root value="outgoing">
|
||||
<Tabs.List>
|
||||
<Tabs.Trigger value="outgoing">Outgoing</Tabs.Trigger>
|
||||
<Tabs.Trigger value="incoming">Incoming</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
|
||||
<Dialog.Content class="sm:max-w-[425px]">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Add a friend</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
Add a friend using their username or manage pending requests.
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
<!-- Outgoing Requests -->
|
||||
<Tabs.Content value="outgoing">
|
||||
{#if user.friendRequests.filter((r) => r.fromUser === user.id).length === 0}
|
||||
<p class="text-sm text-muted-foreground">No outgoing requests</p>
|
||||
{:else}
|
||||
{#each user.friendRequests.filter((r) => r.fromUser === user.id) as request (request.id)}
|
||||
<Card.Root class="mb-2">
|
||||
<Card.Header>
|
||||
<Card.Title>{request.username}</Card.Title>
|
||||
<Card.Description>Request sent</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Footer>
|
||||
<form method="POST" action="?/cancelFriendRequest">
|
||||
<input type="hidden" name="requestId" value={request.id} />
|
||||
<Button type="submit" variant="outline" size="sm">Cancel</Button>
|
||||
</form>
|
||||
</Card.Footer>
|
||||
</Card.Root>
|
||||
{/each}
|
||||
{/if}
|
||||
</Tabs.Content>
|
||||
|
||||
<!-- input to add a new friend -->
|
||||
<form method="POST" action="?/addFriend" class="mb-4">
|
||||
<Input name="username" placeholder="username" required class="mb-2" />
|
||||
<Dialog.Footer>
|
||||
<Dialog.Close class={buttonVariants({ variant: "outline" })}>
|
||||
Cancel
|
||||
</Dialog.Close>
|
||||
<Button type="submit">Send request</Button>
|
||||
</Dialog.Footer>
|
||||
</form>
|
||||
<!-- Incoming Requests -->
|
||||
<Tabs.Content value="incoming">
|
||||
{#if user.friendRequests.filter((r) => r.toUser === user.id).length === 0}
|
||||
<p class="text-sm text-muted-foreground">No incoming requests</p>
|
||||
{:else}
|
||||
{#each user.friendRequests.filter((r) => r.toUser === user.id) as request (request.id)}
|
||||
<Card.Root class="mb-2">
|
||||
<Card.Header>
|
||||
<Card.Title>{request.username}</Card.Title>
|
||||
<Card.Description>Sent you a friend request</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Footer class="flex gap-2">
|
||||
<!-- accept friend -->
|
||||
<form method="POST" action="?/addFriend">
|
||||
<input type="hidden" name="userId" value={request.fromUser} />
|
||||
<Button type="submit" size="sm">Accept</Button>
|
||||
</form>
|
||||
<!-- decline friend -->
|
||||
<form method="POST" action="?/cancelFriendRequest">
|
||||
<input type="hidden" name="requestId" value={request.id} />
|
||||
<Button type="submit" variant="outline" size="sm">Decline</Button>
|
||||
</form>
|
||||
</Card.Footer>
|
||||
</Card.Root>
|
||||
{/each}
|
||||
{/if}
|
||||
</Tabs.Content>
|
||||
</Tabs.Root>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
|
||||
<!-- Tabs for Friend Requests -->
|
||||
<Tabs.Root value="outgoing">
|
||||
<Tabs.List>
|
||||
<Tabs.Trigger value="outgoing">Outgoing</Tabs.Trigger>
|
||||
<Tabs.Trigger value="incoming">Incoming</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
<Dialog.Root>
|
||||
<Dialog.Trigger>
|
||||
<Button variant="outline" size="icon">
|
||||
<UsersRound />
|
||||
</Button>
|
||||
</Dialog.Trigger>
|
||||
|
||||
<!-- Outgoing Requests -->
|
||||
<Tabs.Content value="outgoing">
|
||||
{#if user.friendRequests.filter(r => r.fromUser === user.id).length === 0}
|
||||
<p class="text-sm text-muted-foreground">No outgoing requests</p>
|
||||
{:else}
|
||||
{#each user.friendRequests.filter(r => r.fromUser === user.id) as request (request.id)}
|
||||
<Card.Root class="mb-2">
|
||||
<Card.Header>
|
||||
<Card.Title>{request.username}</Card.Title>
|
||||
<Card.Description>Request sent</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Footer>
|
||||
<form method="POST" action="?/cancelFriendRequest">
|
||||
<input type="hidden" name="requestId" value={request.id} />
|
||||
<Button type="submit" variant="outline" size="sm">
|
||||
Cancel
|
||||
</Button>
|
||||
</form>
|
||||
</Card.Footer>
|
||||
</Card.Root>
|
||||
{/each}
|
||||
{/if}
|
||||
</Tabs.Content>
|
||||
<Dialog.Content class="sm:max-w-[425px]">
|
||||
<form method="POST" action="?/createGroup">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Create a group</Dialog.Title>
|
||||
<Dialog.Description>Add friends into your group!</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
|
||||
<!-- Incoming Requests -->
|
||||
<Tabs.Content value="incoming">
|
||||
{#if user.friendRequests.filter(r => r.toUser === user.id).length === 0}
|
||||
<p class="text-sm text-muted-foreground">No incoming requests</p>
|
||||
{:else}
|
||||
{#each user.friendRequests.filter(r => r.toUser === user.id) as request (request.id)}
|
||||
<Card.Root class="mb-2">
|
||||
<Card.Header>
|
||||
<Card.Title>{request.username}</Card.Title>
|
||||
<Card.Description>Sent you a friend request</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Footer class="flex gap-2">
|
||||
<!-- accept friend -->
|
||||
<form method="POST" action="?/addFriend">
|
||||
<input type="hidden" name="userId" value={request.fromUser} />
|
||||
<Button type="submit" size="sm">Accept</Button>
|
||||
</form>
|
||||
<!-- decline friend -->
|
||||
<form method="POST" action="?/cancelFriendRequest">
|
||||
<input type="hidden" name="requestId" value={request.id} />
|
||||
<Button type="submit" variant="outline" size="sm">Decline</Button>
|
||||
</form>
|
||||
</Card.Footer>
|
||||
</Card.Root>
|
||||
{/each}
|
||||
{/if}
|
||||
</Tabs.Content>
|
||||
</Tabs.Root>
|
||||
</Dialog.Content>
|
||||
{#each data.friends as friend (friend.id)}
|
||||
<label class="flex items-center gap-2">
|
||||
<input type="checkbox" name="member" value={friend.id} />
|
||||
<User user={friend} />
|
||||
</label>
|
||||
{/each}
|
||||
|
||||
<Dialog.Footer>
|
||||
<Dialog.Close class={buttonVariants({ variant: 'outline' })}>Cancel</Dialog.Close>
|
||||
<Button type="submit">Create group</Button>
|
||||
</Dialog.Footer>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
|
||||
</Dialog.Root>
|
||||
<Dialog.Root>
|
||||
<Dialog.Trigger>
|
||||
<Button variant="outline" size="icon">
|
||||
<CirclePlus />
|
||||
</Button>
|
||||
</Dialog.Trigger>
|
||||
|
||||
<Dialog.Root>
|
||||
<Dialog.Trigger>
|
||||
<Button variant="outline" size="icon">
|
||||
<UsersRound />
|
||||
</Button>
|
||||
</Dialog.Trigger>
|
||||
<Dialog.Content class="sm:max-w-[425px]">
|
||||
<form method="POST" action="?/joinServer">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Join a server</Dialog.Title>
|
||||
<Dialog.Description>Enter an invite link.</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
|
||||
<Dialog.Content class="sm:max-w-[425px]">
|
||||
<form method="POST" action="?/createGroup">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Create a group</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
Add friends into your group!
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
<Input name="invite" placeholder="invite link" required />
|
||||
|
||||
{#each data.friends as friend (friend.id)}
|
||||
<label class="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="member"
|
||||
value={friend.id}
|
||||
/>
|
||||
<User user={friend} />
|
||||
</label>
|
||||
{/each}
|
||||
<Dialog.Footer>
|
||||
<Dialog.Close class={buttonVariants({ variant: 'outline' })}>Cancel</Dialog.Close>
|
||||
<Button type="submit">Join</Button>
|
||||
</Dialog.Footer>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
|
||||
<Dialog.Footer>
|
||||
<Dialog.Close class={buttonVariants({ variant: "outline" })}>
|
||||
Cancel
|
||||
</Dialog.Close>
|
||||
<Button type="submit">Create group</Button>
|
||||
</Dialog.Footer>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
<Dialog.Root>
|
||||
<Dialog.Trigger>
|
||||
<Button variant="outline" size="icon">
|
||||
<PlusIcon />
|
||||
</Button>
|
||||
</Dialog.Trigger>
|
||||
|
||||
<Dialog.Root>
|
||||
<Dialog.Trigger>
|
||||
<Button variant="outline" size="icon">
|
||||
<CirclePlus />
|
||||
</Button>
|
||||
</Dialog.Trigger>
|
||||
<Dialog.Content class="sm:max-w-[425px]">
|
||||
<form method="POST" action="?/createServer">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Create a server</Dialog.Title>
|
||||
<Dialog.Description>Name your new server.</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
|
||||
<Dialog.Content class="sm:max-w-[425px]">
|
||||
<form method="POST" action="?/joinServer">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Join a server</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
Enter an invite link.
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
|
||||
<Input name="invite" placeholder="invite link" required />
|
||||
|
||||
<Dialog.Footer>
|
||||
<Dialog.Close class={buttonVariants({ variant: "outline" })}>
|
||||
Cancel
|
||||
</Dialog.Close>
|
||||
<Button type="submit">Join</Button>
|
||||
</Dialog.Footer>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
|
||||
<Dialog.Root>
|
||||
<Dialog.Trigger>
|
||||
<Button variant="outline" size="icon">
|
||||
<PlusIcon />
|
||||
</Button>
|
||||
</Dialog.Trigger>
|
||||
|
||||
<Dialog.Content class="sm:max-w-[425px]">
|
||||
<form method="POST" action="?/createServer">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Create a server</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
Name your new server.
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
|
||||
<Input name="name" placeholder="Server name" required />
|
||||
|
||||
<Dialog.Footer>
|
||||
<Dialog.Close class={buttonVariants({ variant: "outline" })}>
|
||||
Cancel
|
||||
</Dialog.Close>
|
||||
<Button type="submit">Create</Button>
|
||||
</Dialog.Footer>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
<Input name="name" placeholder="Server name" required />
|
||||
|
||||
<Dialog.Footer>
|
||||
<Dialog.Close class={buttonVariants({ variant: 'outline' })}>Cancel</Dialog.Close>
|
||||
<Button type="submit">Create</Button>
|
||||
</Dialog.Footer>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
</div>
|
||||
</Sidebar.MenuItem>
|
||||
</Sidebar.Menu>
|
||||
|
|
@ -230,26 +212,24 @@
|
|||
<Collapsible.Root open={true} class="group/collapsible">
|
||||
<Sidebar.MenuItem>
|
||||
<Collapsible.Trigger>
|
||||
<Sidebar.MenuButton>
|
||||
Friends
|
||||
<PlusIcon
|
||||
class="ms-auto group-data-[state=open]/collapsible:hidden"
|
||||
/>
|
||||
<MinusIcon
|
||||
class="ms-auto group-data-[state=closed]/collapsible:hidden"
|
||||
/>
|
||||
</Sidebar.MenuButton>
|
||||
<Sidebar.MenuButton>
|
||||
Friends
|
||||
<PlusIcon class="ms-auto group-data-[state=open]/collapsible:hidden" />
|
||||
<MinusIcon class="ms-auto group-data-[state=closed]/collapsible:hidden" />
|
||||
</Sidebar.MenuButton>
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Content>
|
||||
<Sidebar.MenuSub>
|
||||
{#each data.friends as friend (friend.id)}
|
||||
<Sidebar.MenuSubItem>
|
||||
<Sidebar.MenuSubButton>
|
||||
|
||||
<User onclick={(e) => {
|
||||
e.preventDefault();
|
||||
currentPage = friend.id;
|
||||
}} user={friend}></User>
|
||||
<User
|
||||
onclick={(e) => {
|
||||
e.preventDefault();
|
||||
currentPage = friend.id;
|
||||
}}
|
||||
user={friend}
|
||||
></User>
|
||||
</Sidebar.MenuSubButton>
|
||||
</Sidebar.MenuSubItem>
|
||||
{/each}
|
||||
|
|
@ -263,12 +243,8 @@
|
|||
<Collapsible.Trigger>
|
||||
<Sidebar.MenuButton>
|
||||
Groups
|
||||
<PlusIcon
|
||||
class="ms-auto group-data-[state=open]/collapsible:hidden"
|
||||
/>
|
||||
<MinusIcon
|
||||
class="ms-auto group-data-[state=closed]/collapsible:hidden"
|
||||
/>
|
||||
<PlusIcon class="ms-auto group-data-[state=open]/collapsible:hidden" />
|
||||
<MinusIcon class="ms-auto group-data-[state=closed]/collapsible:hidden" />
|
||||
</Sidebar.MenuButton>
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Content>
|
||||
|
|
@ -276,10 +252,13 @@
|
|||
{#each data.groups as group (group.id)}
|
||||
<Sidebar.MenuSubItem>
|
||||
<Sidebar.MenuSubButton>
|
||||
<a onclick={(e) => {
|
||||
e.preventDefault();
|
||||
currentPage = group.id;
|
||||
}} href="##">
|
||||
<a
|
||||
onclick={(e) => {
|
||||
e.preventDefault();
|
||||
currentPage = group.id;
|
||||
}}
|
||||
href="##"
|
||||
>
|
||||
{group.name} ({group.members} members)
|
||||
</a>
|
||||
</Sidebar.MenuSubButton>
|
||||
|
|
@ -295,12 +274,8 @@
|
|||
<Collapsible.Trigger>
|
||||
<Sidebar.MenuButton>
|
||||
Servers
|
||||
<PlusIcon
|
||||
class="ms-auto group-data-[state=open]/collapsible:hidden"
|
||||
/>
|
||||
<MinusIcon
|
||||
class="ms-auto group-data-[state=closed]/collapsible:hidden"
|
||||
/>
|
||||
<PlusIcon class="ms-auto group-data-[state=open]/collapsible:hidden" />
|
||||
<MinusIcon class="ms-auto group-data-[state=closed]/collapsible:hidden" />
|
||||
</Sidebar.MenuButton>
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Content>
|
||||
|
|
@ -308,11 +283,19 @@
|
|||
{#each data.servers as server (server.id)}
|
||||
<Sidebar.MenuSubItem>
|
||||
<Sidebar.MenuSubButton>
|
||||
<a onclick={(e) => {
|
||||
e.preventDefault();
|
||||
currentPage = server.id;
|
||||
}} href="##" class="flex items-center gap-2">
|
||||
<img src={"https://api.dicebear.com/7.x/pixel-art/svg?seed=" + server.name} alt={server.name} class="size-6 rounded-full" />
|
||||
<a
|
||||
onclick={(e) => {
|
||||
e.preventDefault();
|
||||
currentPage = server.id;
|
||||
}}
|
||||
href="##"
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
<img
|
||||
src={'https://api.dicebear.com/7.x/pixel-art/svg?seed=' + server.name}
|
||||
alt={server.name}
|
||||
class="size-6 rounded-full"
|
||||
/>
|
||||
{server.name}
|
||||
</a>
|
||||
</Sidebar.MenuSubButton>
|
||||
|
|
|
|||
9
src/lib/components/hooks/is-mobile.svelte.ts
Normal file
9
src/lib/components/hooks/is-mobile.svelte.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { MediaQuery } from "svelte/reactivity";
|
||||
|
||||
const DEFAULT_MOBILE_BREAKPOINT = 768;
|
||||
|
||||
export class IsMobile extends MediaQuery {
|
||||
constructor(breakpoint: number = DEFAULT_MOBILE_BREAKPOINT) {
|
||||
super(`max-width: ${breakpoint - 1}px`);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,48 +1,48 @@
|
|||
import { definePrefix, type Puuid } from "./puuid"
|
||||
import { definePrefix, type Puuid } from './puuid';
|
||||
|
||||
export const UserID = definePrefix("user");
|
||||
export const GroupID = definePrefix("group");
|
||||
export const ServerID = definePrefix("srv");
|
||||
export const FriendRequestID = definePrefix("frq");
|
||||
export const UserID = definePrefix('user');
|
||||
export const GroupID = definePrefix('group');
|
||||
export const ServerID = definePrefix('srv');
|
||||
export const FriendRequestID = definePrefix('frq');
|
||||
|
||||
export type UserId = Puuid<"user">;
|
||||
export type GroupId = Puuid<"group">;
|
||||
export type ServerId = Puuid<"srv">;
|
||||
export type FriendRequestID = Puuid<"frq">;
|
||||
export type UserId = Puuid<'user'>;
|
||||
export type GroupId = Puuid<'group'>;
|
||||
export type ServerId = Puuid<'srv'>;
|
||||
export type FriendRequestID = Puuid<'frq'>;
|
||||
|
||||
export const Status: Record<string, 1|2|3> = {
|
||||
export const Status: Record<string, 1 | 2 | 3> = {
|
||||
OFFLINE: 1,
|
||||
DND: 2,
|
||||
ONLINE: 3
|
||||
}
|
||||
};
|
||||
|
||||
export type OverviewUser = {
|
||||
id: string;
|
||||
username: string;
|
||||
image: string;
|
||||
id: string;
|
||||
username: string;
|
||||
image: string;
|
||||
};
|
||||
|
||||
export type OverviewServer = {
|
||||
id: string;
|
||||
name: string;
|
||||
id: string;
|
||||
name: string;
|
||||
ownerId: string;
|
||||
image: string;
|
||||
};
|
||||
export type OverviewGroup = {
|
||||
id: string;
|
||||
name: string;
|
||||
id: string;
|
||||
name: string;
|
||||
ownerId: string;
|
||||
members: number;
|
||||
image: string;
|
||||
};
|
||||
|
||||
export interface OverviewData {
|
||||
friends: OverviewUser[],
|
||||
groups: OverviewGroup[],
|
||||
servers: OverviewServer[],
|
||||
};
|
||||
|
||||
export interface UserWithStatus extends OverviewUser {
|
||||
status: 1|2|3,
|
||||
statusMessage: string
|
||||
status: 1 | 2 | 3;
|
||||
statusMessage: string;
|
||||
}
|
||||
|
||||
export interface OverviewData {
|
||||
friends: UserWithStatus[];
|
||||
groups: OverviewGroup[];
|
||||
servers: OverviewServer[];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,42 +1,36 @@
|
|||
import { v4 as uuidv4 } from "uuid";
|
||||
import { v7 as uuidv7 } from "uuid";
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { v7 as uuidv7 } from 'uuid';
|
||||
|
||||
type Brand<K, T> = K & { __brand: T };
|
||||
|
||||
export type Puuid<Prefix extends string> = Brand<
|
||||
string,
|
||||
{ prefix: Prefix }
|
||||
>;
|
||||
export type Puuid<Prefix extends string> = Brand<string, { prefix: Prefix }>;
|
||||
|
||||
export function definePrefix<const P extends string>(prefix: P) {
|
||||
const withPrefix = (uuid: string) =>
|
||||
`${prefix}_${uuid}` as Puuid<P>;
|
||||
const withPrefix = (uuid: string) => `${prefix}_${uuid}` as Puuid<P>;
|
||||
|
||||
return {
|
||||
prefix,
|
||||
is(value: string): value is Puuid<P> {
|
||||
return value.startsWith(prefix + "_");
|
||||
},
|
||||
return {
|
||||
prefix,
|
||||
is(value: string): value is Puuid<P> {
|
||||
return value.startsWith(prefix + '_');
|
||||
},
|
||||
|
||||
newV7(): Puuid<P> {
|
||||
return withPrefix(uuidv7());
|
||||
},
|
||||
newV7(): Puuid<P> {
|
||||
return withPrefix(uuidv7());
|
||||
},
|
||||
|
||||
newV4(): Puuid<P> {
|
||||
return withPrefix(uuidv4());
|
||||
},
|
||||
newV4(): Puuid<P> {
|
||||
return withPrefix(uuidv4());
|
||||
},
|
||||
|
||||
parse(value: string): Puuid<P> {
|
||||
if (!value.startsWith(prefix + "_")) {
|
||||
throw new Error(
|
||||
`Invalid prefix, expected "${prefix}_"`
|
||||
);
|
||||
}
|
||||
return value as Puuid<P>;
|
||||
},
|
||||
parse(value: string): Puuid<P> {
|
||||
if (!value.startsWith(prefix + '_')) {
|
||||
throw new Error(`Invalid prefix, expected "${prefix}_"`);
|
||||
}
|
||||
return value as Puuid<P>;
|
||||
},
|
||||
|
||||
inner(id: Puuid<P>): string {
|
||||
return id.slice(prefix.length + 1);
|
||||
},
|
||||
};
|
||||
inner(id: Puuid<P>): string {
|
||||
return id.slice(prefix.length + 1);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,141 +10,151 @@ 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;
|
||||
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: new Date(Date.now() + DAY_IN_MS * 30)
|
||||
};
|
||||
await db.insert(table.session).values(session);
|
||||
return session;
|
||||
const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)));
|
||||
const session: table.Session = {
|
||||
id: sessionId,
|
||||
userId,
|
||||
expiresAt: new Date(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 [row] = await db
|
||||
.select({
|
||||
user: {
|
||||
id: table.user.id,
|
||||
username: table.user.username,
|
||||
friends: table.user.friends,
|
||||
servers: table.user.servers,
|
||||
groups: table.user.groups,
|
||||
},
|
||||
session: table.session,
|
||||
})
|
||||
.from(table.session)
|
||||
.innerJoin(table.user, eq(table.session.userId, table.user.id))
|
||||
.where(eq(table.session.id, sessionId));
|
||||
const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)));
|
||||
const [row] = await db
|
||||
.select({
|
||||
user: {
|
||||
id: table.user.id,
|
||||
username: table.user.username,
|
||||
friends: table.user.friends,
|
||||
servers: table.user.servers,
|
||||
groups: table.user.groups,
|
||||
statusOverwrite: table.user.statusOverwrite
|
||||
},
|
||||
session: table.session
|
||||
})
|
||||
.from(table.session)
|
||||
.innerJoin(table.user, eq(table.session.userId, table.user.id))
|
||||
.where(eq(table.session.id, sessionId));
|
||||
|
||||
if (!row) {
|
||||
return { session: null, user: null };
|
||||
}
|
||||
const { session, user } = row;
|
||||
if (!row) {
|
||||
return { session: null, user: null };
|
||||
}
|
||||
const { session, user } = row;
|
||||
|
||||
const sessionExpired = Date.now() >= session.expiresAt.getTime();
|
||||
if (sessionExpired) {
|
||||
await db.delete(table.session).where(eq(table.session.id, session.id));
|
||||
return { session: null, user: null };
|
||||
}
|
||||
const sessionExpired = Date.now() >= session.expiresAt.getTime();
|
||||
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.getTime() - DAY_IN_MS * 15;
|
||||
if (renewSession) {
|
||||
session.expiresAt = new Date(Date.now() + DAY_IN_MS * 30);
|
||||
await db
|
||||
.update(table.session)
|
||||
.set({ expiresAt: session.expiresAt })
|
||||
.where(eq(table.session.id, session.id));
|
||||
}
|
||||
const friends = (user.friends as string[]).length
|
||||
? await db
|
||||
.select({
|
||||
id: table.user.id,
|
||||
username: table.user.username,
|
||||
})
|
||||
.from(table.user)
|
||||
.where(inArray(table.user.id, (user.friends as string[])))
|
||||
: [];
|
||||
const renewSession = Date.now() >= session.expiresAt.getTime() - DAY_IN_MS * 15;
|
||||
if (renewSession) {
|
||||
session.expiresAt = new Date(Date.now() + DAY_IN_MS * 30);
|
||||
await db
|
||||
.update(table.session)
|
||||
.set({ expiresAt: session.expiresAt })
|
||||
.where(eq(table.session.id, session.id));
|
||||
}
|
||||
const friends = (user.friends as string[]).length
|
||||
? await db
|
||||
.select({
|
||||
id: table.user.id,
|
||||
username: table.user.username
|
||||
})
|
||||
.from(table.user)
|
||||
.where(inArray(table.user.id, user.friends as string[]))
|
||||
: [];
|
||||
|
||||
const servers = (user.servers as string[]).length
|
||||
? await db
|
||||
.select({
|
||||
id: table.server.id,
|
||||
name: table.server.name,
|
||||
ownerId: table.server.owner,
|
||||
})
|
||||
.from(table.server)
|
||||
.where(inArray(table.server.id, (user.servers as string[])))
|
||||
: [];
|
||||
const groups = (user.groups as string[]).length
|
||||
? await db
|
||||
.select({
|
||||
id: table.group.id,
|
||||
name: table.group.name,
|
||||
ownerId: table.group.owner,
|
||||
members: table.group.members
|
||||
})
|
||||
.from(table.group)
|
||||
.where(inArray(table.group.id, (user.groups as string[])))
|
||||
: [];
|
||||
const servers = (user.servers as string[]).length
|
||||
? await db
|
||||
.select({
|
||||
id: table.server.id,
|
||||
name: table.server.name,
|
||||
ownerId: table.server.owner
|
||||
})
|
||||
.from(table.server)
|
||||
.where(inArray(table.server.id, user.servers as string[]))
|
||||
: [];
|
||||
const groups = (user.groups as string[]).length
|
||||
? await db
|
||||
.select({
|
||||
id: table.group.id,
|
||||
name: table.group.name,
|
||||
ownerId: table.group.owner,
|
||||
members: table.group.members
|
||||
})
|
||||
.from(table.group)
|
||||
.where(inArray(table.group.id, user.groups as string[]))
|
||||
: [];
|
||||
|
||||
const friendRequests = await db
|
||||
.select({
|
||||
id: table.friendRequest.id,
|
||||
fromUser: table.friendRequest.fromUser,
|
||||
toUser: table.friendRequest.toUser,
|
||||
})
|
||||
.from(table.friendRequest)
|
||||
.where(or(eq(table.friendRequest.fromUser, user.id), eq(table.friendRequest.toUser, user.id)))
|
||||
const friendRequests = await db
|
||||
.select({
|
||||
id: table.friendRequest.id,
|
||||
fromUser: table.friendRequest.fromUser,
|
||||
toUser: table.friendRequest.toUser
|
||||
})
|
||||
.from(table.friendRequest)
|
||||
.where(or(eq(table.friendRequest.fromUser, user.id), eq(table.friendRequest.toUser, user.id)));
|
||||
|
||||
|
||||
return { session, user: {...user, servers, friends, groups: groups.map(z => { return { ...z, members: (z.members as string[]).length}}), friendRequests} };
|
||||
return {
|
||||
session,
|
||||
user: {
|
||||
...user,
|
||||
servers,
|
||||
friends,
|
||||
groups: groups.map((z) => {
|
||||
return { ...z, members: (z.members as string[]).length };
|
||||
}),
|
||||
friendRequests
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export type SessionValidationResult = Awaited<ReturnType<typeof validateSessionToken>>;
|
||||
|
||||
export async function invalidateSession(sessionId: string) {
|
||||
await db.delete(table.session).where(eq(table.session.id, sessionId));
|
||||
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: '/'
|
||||
});
|
||||
event.cookies.set(sessionCookieName, token, {
|
||||
expires: expiresAt,
|
||||
path: '/'
|
||||
});
|
||||
}
|
||||
|
||||
export function deleteSessionTokenCookie(event: RequestEvent) {
|
||||
event.cookies.delete(sessionCookieName, {
|
||||
path: '/'
|
||||
});
|
||||
event.cookies.delete(sessionCookieName, {
|
||||
path: '/'
|
||||
});
|
||||
}
|
||||
|
||||
export function validateUsername(username: unknown): username is string {
|
||||
return (
|
||||
typeof username === 'string' &&
|
||||
username.length >= 3 &&
|
||||
username.length <= 31 &&
|
||||
/^[a-z0-9_-]+$/.test(username)
|
||||
);
|
||||
return (
|
||||
typeof username === 'string' &&
|
||||
username.length >= 3 &&
|
||||
username.length <= 31 &&
|
||||
/^[a-z0-9_-]+$/.test(username)
|
||||
);
|
||||
}
|
||||
|
||||
export function validatePassword(password: unknown): password is string {
|
||||
return typeof password === 'string' && password.length >= 6 && password.length <= 255;
|
||||
return typeof password === 'string' && password.length >= 6 && password.length <= 255;
|
||||
}
|
||||
|
||||
|
||||
export function validateEmail(email: unknown): email is string {
|
||||
return (
|
||||
typeof email === 'string' &&
|
||||
email.length >= 5 &&
|
||||
email.length <= 254 &&
|
||||
/^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$/i.test(email)
|
||||
);
|
||||
return (
|
||||
typeof email === 'string' &&
|
||||
email.length >= 5 &&
|
||||
email.length <= 254 &&
|
||||
/^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$/i.test(email)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { drizzle } from 'drizzle-orm/bun-sqlite';
|
||||
import { Database } from 'bun:sqlite';
|
||||
import { BunSqliteKeyValue } from "bun-sqlite-key-value"
|
||||
import { BunSqliteKeyValue } from 'bun-sqlite-key-value';
|
||||
|
||||
const sqlite = new Database('database.db');
|
||||
export const db = drizzle(sqlite);
|
||||
|
||||
export const kvStore = new BunSqliteKeyValue("./kvStore.db")
|
||||
export const kvStore = new BunSqliteKeyValue('./kvStore.db');
|
||||
|
|
|
|||
|
|
@ -1,14 +1,16 @@
|
|||
import { Status } from '../../index.ts';
|
||||
import { sqliteTable, integer, text } from 'drizzle-orm/sqlite-core';
|
||||
|
||||
export const user = sqliteTable('user', {
|
||||
id: text('id').primaryKey(),
|
||||
username: text('username').notNull().unique(),
|
||||
email: text('email').notNull().unique(),
|
||||
passwordHash: text('password_hash').notNull(),
|
||||
friends: text('friends', { mode: "json"}).default([]).notNull(),
|
||||
id: text('id').primaryKey(),
|
||||
username: text('username').notNull().unique(),
|
||||
email: text('email').notNull().unique(),
|
||||
passwordHash: text('password_hash').notNull(),
|
||||
statusOverwrite: integer('status_overwrite').default(Status.ONLINE).notNull(),
|
||||
friends: text('friends', { mode: 'json' }).default([]).notNull(),
|
||||
|
||||
servers: text('servers', { mode: "json"}).default([]).notNull(), // string[] of ServerIDs
|
||||
groups: text('groups', { mode: "json"}).default([]).notNull(), // string[] of GroupIDs
|
||||
servers: text('servers', { mode: 'json' }).default([]).notNull(), // string[] of ServerIDs
|
||||
groups: text('groups', { mode: 'json' }).default([]).notNull() // string[] of GroupIDs
|
||||
});
|
||||
|
||||
export const session = sqliteTable('session', {
|
||||
|
|
@ -19,39 +21,66 @@ export const session = sqliteTable('session', {
|
|||
expiresAt: integer('expires_at', { mode: 'timestamp' }).notNull()
|
||||
});
|
||||
|
||||
export const server = sqliteTable("server", {
|
||||
export const server = sqliteTable('server', {
|
||||
id: text('id').primaryKey(),
|
||||
name: text('name').notNull(),
|
||||
owner: text('owner').notNull().references(() => user.id),
|
||||
members: text('members', { mode: "json"}).default([]).notNull(),
|
||||
channels: text('channels', { mode: "json"}).default([]).notNull(), // string[] of ChannelIDs
|
||||
})
|
||||
owner: text('owner')
|
||||
.notNull()
|
||||
.references(() => user.id),
|
||||
members: text('members', { mode: 'json' }).default([]).notNull(),
|
||||
channels: text('channels', { mode: 'json' }).default([]).notNull() // string[] of ChannelIDs
|
||||
});
|
||||
|
||||
export const group = sqliteTable("group", {
|
||||
export const group = sqliteTable('group', {
|
||||
id: text('id').primaryKey(),
|
||||
name: text('name').notNull(),
|
||||
owner: text('owner').notNull().references(() => user.id),
|
||||
members: text('members', { mode: "json"}).default([]).notNull(),
|
||||
})
|
||||
owner: text('owner')
|
||||
.notNull()
|
||||
.references(() => user.id),
|
||||
members: text('members', { mode: 'json' }).default([]).notNull()
|
||||
});
|
||||
|
||||
export const channel = sqliteTable("channel", {
|
||||
export const channel = sqliteTable('channel', {
|
||||
id: text('id').primaryKey(),
|
||||
name: text('name').notNull(),
|
||||
serverId: text('server_id').notNull().references(() => server.id),
|
||||
messages: text('messages', { mode: "json"}).default([]).notNull(),
|
||||
})
|
||||
serverId: text('server_id')
|
||||
.notNull()
|
||||
.references(() => server.id),
|
||||
messages: text('messages', { mode: 'json' }).default([]).notNull()
|
||||
});
|
||||
|
||||
export const friendRequest = sqliteTable("friendRequest", {
|
||||
export const directMessage = sqliteTable('directMessage', {
|
||||
id: text('id').primaryKey(),
|
||||
fromUser: text('from_user').notNull().references(() => user.id),
|
||||
toUser: text('to_user').notNull().references(() => user.id),
|
||||
})
|
||||
name: text('name').notNull(),
|
||||
serverId: text('server_id')
|
||||
.notNull()
|
||||
.references(() => server.id),
|
||||
messages: text('messages', { mode: 'json' }).default([]).notNull()
|
||||
});
|
||||
|
||||
export const invite = sqliteTable("invite", {
|
||||
export const friendRequest = sqliteTable('friendRequest', {
|
||||
id: text('id').primaryKey(),
|
||||
serverId: text('server_id').notNull().references(() => server.id),
|
||||
code: text('code').notNull(),
|
||||
})
|
||||
fromUser: text('from_user')
|
||||
.notNull()
|
||||
.references(() => user.id),
|
||||
fromUsername: text('from_username')
|
||||
.notNull()
|
||||
.references(() => user.id),
|
||||
toUsername: text('to_username')
|
||||
.notNull()
|
||||
.references(() => user.id),
|
||||
toUser: text('to_user')
|
||||
.notNull()
|
||||
.references(() => user.id)
|
||||
});
|
||||
|
||||
export const invite = sqliteTable('invite', {
|
||||
id: text('id').primaryKey(),
|
||||
serverId: text('server_id')
|
||||
.notNull()
|
||||
.references(() => server.id),
|
||||
code: text('code').notNull()
|
||||
});
|
||||
export type Session = typeof session.$inferSelect;
|
||||
export type User = typeof user.$inferSelect;
|
||||
export type Group = typeof group.$inferSelect;
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { clsx, type ClassValue } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type WithoutChild<T> = T extends { child?: any } ? Omit<T, "child"> : T;
|
||||
export type WithoutChild<T> = T extends { child?: any } ? Omit<T, 'child'> : T;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type WithoutChildren<T> = T extends { children?: any } ? Omit<T, "children"> : T;
|
||||
export type WithoutChildren<T> = T extends { children?: any } ? Omit<T, 'children'> : T;
|
||||
export type WithoutChildrenOrChild<T> = WithoutChildren<WithoutChild<T>>;
|
||||
export type WithElementRef<T, U extends HTMLElement = HTMLElement> = T & { ref?: U | null };
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue