get the whole friends system to work now
This commit is contained in:
parent
342fd30d62
commit
126acf52f3
34 changed files with 1101 additions and 40 deletions
7
drizzle/0006_gifted_machine_man.sql
Normal file
7
drizzle/0006_gifted_machine_man.sql
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
CREATE TABLE `friendRequest` (
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`from_user` text NOT NULL,
|
||||||
|
`to_user` text NOT NULL,
|
||||||
|
FOREIGN KEY (`from_user`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action,
|
||||||
|
FOREIGN KEY (`to_user`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action
|
||||||
|
);
|
||||||
365
drizzle/meta/0006_snapshot.json
Normal file
365
drizzle/meta/0006_snapshot.json
Normal file
|
|
@ -0,0 +1,365 @@
|
||||||
|
{
|
||||||
|
"version": "6",
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"id": "e809c266-891b-4355-a87c-facd55a5293a",
|
||||||
|
"prevId": "69be2f8f-70ca-4016-b10f-60f64a99af73",
|
||||||
|
"tables": {
|
||||||
|
"channel": {
|
||||||
|
"name": "channel",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"server_id": {
|
||||||
|
"name": "server_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"messages": {
|
||||||
|
"name": "messages",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'[]'"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"channel_server_id_server_id_fk": {
|
||||||
|
"name": "channel_server_id_server_id_fk",
|
||||||
|
"tableFrom": "channel",
|
||||||
|
"tableTo": "server",
|
||||||
|
"columnsFrom": [
|
||||||
|
"server_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"friendRequest": {
|
||||||
|
"name": "friendRequest",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"from_user": {
|
||||||
|
"name": "from_user",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"to_user": {
|
||||||
|
"name": "to_user",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"friendRequest_from_user_user_id_fk": {
|
||||||
|
"name": "friendRequest_from_user_user_id_fk",
|
||||||
|
"tableFrom": "friendRequest",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"from_user"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"friendRequest_to_user_user_id_fk": {
|
||||||
|
"name": "friendRequest_to_user_user_id_fk",
|
||||||
|
"tableFrom": "friendRequest",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"to_user"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"group": {
|
||||||
|
"name": "group",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"owner": {
|
||||||
|
"name": "owner",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"members": {
|
||||||
|
"name": "members",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'[]'"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"group_owner_user_id_fk": {
|
||||||
|
"name": "group_owner_user_id_fk",
|
||||||
|
"tableFrom": "group",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"owner"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"server": {
|
||||||
|
"name": "server",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"owner": {
|
||||||
|
"name": "owner",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"members": {
|
||||||
|
"name": "members",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'[]'"
|
||||||
|
},
|
||||||
|
"channels": {
|
||||||
|
"name": "channels",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'[]'"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"server_owner_user_id_fk": {
|
||||||
|
"name": "server_owner_user_id_fk",
|
||||||
|
"tableFrom": "server",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"owner"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"session": {
|
||||||
|
"name": "session",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"expires_at": {
|
||||||
|
"name": "expires_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"session_user_id_user_id_fk": {
|
||||||
|
"name": "session_user_id_user_id_fk",
|
||||||
|
"tableFrom": "session",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"name": "user",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"username": {
|
||||||
|
"name": "username",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"password_hash": {
|
||||||
|
"name": "password_hash",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"friends": {
|
||||||
|
"name": "friends",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'[]'"
|
||||||
|
},
|
||||||
|
"servers": {
|
||||||
|
"name": "servers",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'[]'"
|
||||||
|
},
|
||||||
|
"groups": {
|
||||||
|
"name": "groups",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'[]'"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"user_username_unique": {
|
||||||
|
"name": "user_username_unique",
|
||||||
|
"columns": [
|
||||||
|
"username"
|
||||||
|
],
|
||||||
|
"isUnique": true
|
||||||
|
},
|
||||||
|
"user_email_unique": {
|
||||||
|
"name": "user_email_unique",
|
||||||
|
"columns": [
|
||||||
|
"email"
|
||||||
|
],
|
||||||
|
"isUnique": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"views": {},
|
||||||
|
"enums": {},
|
||||||
|
"_meta": {
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {},
|
||||||
|
"columns": {}
|
||||||
|
},
|
||||||
|
"internal": {
|
||||||
|
"indexes": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -43,6 +43,13 @@
|
||||||
"when": 1767531680914,
|
"when": 1767531680914,
|
||||||
"tag": "0005_mute_mephisto",
|
"tag": "0005_mute_mephisto",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 6,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1767537153441,
|
||||||
|
"tag": "0006_gifted_machine_man",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { type Data } from "$lib";
|
import { type Data, type OverviewUser } from "$lib";
|
||||||
import * as Collapsible from "$lib/components/ui/collapsible/index.js";
|
import * as Collapsible from "$lib/components/ui/collapsible/index.js";
|
||||||
import * as Sidebar from "$lib/components/ui/sidebar/index.js";
|
import * as Sidebar from "$lib/components/ui/sidebar/index.js";
|
||||||
import * as Dialog from "$lib/components/ui/dialog/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 MessagesSquare from "@lucide/svelte/icons/messages-square";
|
||||||
import MinusIcon from "@lucide/svelte/icons/minus";
|
import MinusIcon from "@lucide/svelte/icons/minus";
|
||||||
import PlusIcon from "@lucide/svelte/icons/plus";
|
import PlusIcon from "@lucide/svelte/icons/plus";
|
||||||
|
|
@ -12,8 +14,9 @@
|
||||||
import Input from "./ui/input/input.svelte";
|
import Input from "./ui/input/input.svelte";
|
||||||
import Button, { buttonVariants } from "./ui/button/button.svelte";
|
import Button, { buttonVariants } from "./ui/button/button.svelte";
|
||||||
import User from "./extra/User.svelte";
|
import User from "./extra/User.svelte";
|
||||||
|
import type { SessionValidationResult } from "$lib/server/auth";
|
||||||
|
|
||||||
let { currentPage = $bindable<string|null>(), data, ...restProps }: {currentPage: string|null, data: Data }= $props();
|
let { currentPage = $bindable<string|null>(), data, user, ...restProps }: {currentPage: string|null, data: Data, user: SessionValidationResult['user'] }= $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Sidebar.Root {...restProps}>
|
<Sidebar.Root {...restProps}>
|
||||||
|
|
@ -34,22 +37,24 @@
|
||||||
|
|
||||||
<Dialog.Root>
|
<Dialog.Root>
|
||||||
<Dialog.Trigger>
|
<Dialog.Trigger>
|
||||||
<Button variant="outline" size="icon">
|
<Button variant={user!.friendRequests.length > 0 ? "destructive" : "outline"} size="icon">
|
||||||
<UserRoundPlus />
|
<UserRoundPlus />
|
||||||
|
|
||||||
</Button>
|
</Button>
|
||||||
</Dialog.Trigger>
|
</Dialog.Trigger>
|
||||||
|
|
||||||
|
|
||||||
<Dialog.Content class="sm:max-w-[425px]">
|
<Dialog.Content class="sm:max-w-[425px]">
|
||||||
<form method="POST" action="?/addFriend">
|
|
||||||
<Dialog.Header>
|
<Dialog.Header>
|
||||||
<Dialog.Title>Add a friend</Dialog.Title>
|
<Dialog.Title>Add a friend</Dialog.Title>
|
||||||
<Dialog.Description>
|
<Dialog.Description>
|
||||||
Add a friend using their username.
|
Add a friend using their username or manage pending requests.
|
||||||
</Dialog.Description>
|
</Dialog.Description>
|
||||||
</Dialog.Header>
|
</Dialog.Header>
|
||||||
|
|
||||||
<Input name="username" placeholder="username" required />
|
<!-- 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.Footer>
|
||||||
<Dialog.Close class={buttonVariants({ variant: "outline" })}>
|
<Dialog.Close class={buttonVariants({ variant: "outline" })}>
|
||||||
Cancel
|
Cancel
|
||||||
|
|
@ -57,7 +62,69 @@
|
||||||
<Button type="submit">Send request</Button>
|
<Button type="submit">Send request</Button>
|
||||||
</Dialog.Footer>
|
</Dialog.Footer>
|
||||||
</form>
|
</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>
|
||||||
|
|
||||||
|
<!-- 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>
|
||||||
|
|
||||||
|
<!-- 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.Content>
|
||||||
|
|
||||||
|
|
||||||
</Dialog.Root>
|
</Dialog.Root>
|
||||||
|
|
||||||
<Dialog.Root>
|
<Dialog.Root>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
||||||
|
import { buttonVariants } from "$lib/components/ui/button/index.js";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: AlertDialogPrimitive.ActionProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<AlertDialogPrimitive.Action
|
||||||
|
bind:ref
|
||||||
|
data-slot="alert-dialog-action"
|
||||||
|
class={cn(buttonVariants(), className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
||||||
|
import { buttonVariants } from "$lib/components/ui/button/index.js";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: AlertDialogPrimitive.CancelProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<AlertDialogPrimitive.Cancel
|
||||||
|
bind:ref
|
||||||
|
data-slot="alert-dialog-cancel"
|
||||||
|
class={cn(buttonVariants({ variant: "outline" }), className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
||||||
|
import AlertDialogPortal from "./alert-dialog-portal.svelte";
|
||||||
|
import AlertDialogOverlay from "./alert-dialog-overlay.svelte";
|
||||||
|
import { cn, type WithoutChild, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||||
|
import type { ComponentProps } from "svelte";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
portalProps,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChild<AlertDialogPrimitive.ContentProps> & {
|
||||||
|
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof AlertDialogPortal>>;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<AlertDialogPortal {...portalProps}>
|
||||||
|
<AlertDialogOverlay />
|
||||||
|
<AlertDialogPrimitive.Content
|
||||||
|
bind:ref
|
||||||
|
data-slot="alert-dialog-content"
|
||||||
|
class={cn(
|
||||||
|
"bg-background 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 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
|
</AlertDialogPortal>
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: AlertDialogPrimitive.DescriptionProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<AlertDialogPrimitive.Description
|
||||||
|
bind:ref
|
||||||
|
data-slot="alert-dialog-description"
|
||||||
|
class={cn("text-muted-foreground text-sm", className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="alert-dialog-footer"
|
||||||
|
class={cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="alert-dialog-header"
|
||||||
|
class={cn("flex flex-col gap-2 text-center sm:text-start", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: AlertDialogPrimitive.OverlayProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<AlertDialogPrimitive.Overlay
|
||||||
|
bind:ref
|
||||||
|
data-slot="alert-dialog-overlay"
|
||||||
|
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/50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
||||||
|
|
||||||
|
let { ...restProps }: AlertDialogPrimitive.PortalProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<AlertDialogPrimitive.Portal {...restProps} />
|
||||||
17
src/lib/components/ui/alert-dialog/alert-dialog-title.svelte
Normal file
17
src/lib/components/ui/alert-dialog/alert-dialog-title.svelte
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: AlertDialogPrimitive.TitleProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<AlertDialogPrimitive.Title
|
||||||
|
bind:ref
|
||||||
|
data-slot="alert-dialog-title"
|
||||||
|
class={cn("text-lg font-semibold", className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
||||||
|
|
||||||
|
let { ref = $bindable(null), ...restProps }: AlertDialogPrimitive.TriggerProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<AlertDialogPrimitive.Trigger bind:ref data-slot="alert-dialog-trigger" {...restProps} />
|
||||||
7
src/lib/components/ui/alert-dialog/alert-dialog.svelte
Normal file
7
src/lib/components/ui/alert-dialog/alert-dialog.svelte
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
||||||
|
|
||||||
|
let { open = $bindable(false), ...restProps }: AlertDialogPrimitive.RootProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<AlertDialogPrimitive.Root bind:open {...restProps} />
|
||||||
37
src/lib/components/ui/alert-dialog/index.ts
Normal file
37
src/lib/components/ui/alert-dialog/index.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
import Root from "./alert-dialog.svelte";
|
||||||
|
import Portal from "./alert-dialog-portal.svelte";
|
||||||
|
import Trigger from "./alert-dialog-trigger.svelte";
|
||||||
|
import Title from "./alert-dialog-title.svelte";
|
||||||
|
import Action from "./alert-dialog-action.svelte";
|
||||||
|
import Cancel from "./alert-dialog-cancel.svelte";
|
||||||
|
import Footer from "./alert-dialog-footer.svelte";
|
||||||
|
import Header from "./alert-dialog-header.svelte";
|
||||||
|
import Overlay from "./alert-dialog-overlay.svelte";
|
||||||
|
import Content from "./alert-dialog-content.svelte";
|
||||||
|
import Description from "./alert-dialog-description.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
Title,
|
||||||
|
Action,
|
||||||
|
Cancel,
|
||||||
|
Portal,
|
||||||
|
Footer,
|
||||||
|
Header,
|
||||||
|
Trigger,
|
||||||
|
Overlay,
|
||||||
|
Content,
|
||||||
|
Description,
|
||||||
|
//
|
||||||
|
Root as AlertDialog,
|
||||||
|
Title as AlertDialogTitle,
|
||||||
|
Action as AlertDialogAction,
|
||||||
|
Cancel as AlertDialogCancel,
|
||||||
|
Portal as AlertDialogPortal,
|
||||||
|
Footer as AlertDialogFooter,
|
||||||
|
Header as AlertDialogHeader,
|
||||||
|
Trigger as AlertDialogTrigger,
|
||||||
|
Overlay as AlertDialogOverlay,
|
||||||
|
Content as AlertDialogContent,
|
||||||
|
Description as AlertDialogDescription,
|
||||||
|
};
|
||||||
20
src/lib/components/ui/card/card-action.svelte
Normal file
20
src/lib/components/ui/card/card-action.svelte
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="card-action"
|
||||||
|
class={cn("col-start-2 row-span-2 row-start-1 self-start justify-self-end", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
15
src/lib/components/ui/card/card-content.svelte
Normal file
15
src/lib/components/ui/card/card-content.svelte
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div bind:this={ref} data-slot="card-content" class={cn("px-6", className)} {...restProps}>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
20
src/lib/components/ui/card/card-description.svelte
Normal file
20
src/lib/components/ui/card/card-description.svelte
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLParagraphElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<p
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="card-description"
|
||||||
|
class={cn("text-muted-foreground text-sm", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</p>
|
||||||
20
src/lib/components/ui/card/card-footer.svelte
Normal file
20
src/lib/components/ui/card/card-footer.svelte
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="card-footer"
|
||||||
|
class={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
23
src/lib/components/ui/card/card-header.svelte
Normal file
23
src/lib/components/ui/card/card-header.svelte
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="card-header"
|
||||||
|
class={cn(
|
||||||
|
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
20
src/lib/components/ui/card/card-title.svelte
Normal file
20
src/lib/components/ui/card/card-title.svelte
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="card-title"
|
||||||
|
class={cn("leading-none font-semibold", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
23
src/lib/components/ui/card/card.svelte
Normal file
23
src/lib/components/ui/card/card.svelte
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="card"
|
||||||
|
class={cn(
|
||||||
|
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
25
src/lib/components/ui/card/index.ts
Normal file
25
src/lib/components/ui/card/index.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
import Root from "./card.svelte";
|
||||||
|
import Content from "./card-content.svelte";
|
||||||
|
import Description from "./card-description.svelte";
|
||||||
|
import Footer from "./card-footer.svelte";
|
||||||
|
import Header from "./card-header.svelte";
|
||||||
|
import Title from "./card-title.svelte";
|
||||||
|
import Action from "./card-action.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
Content,
|
||||||
|
Description,
|
||||||
|
Footer,
|
||||||
|
Header,
|
||||||
|
Title,
|
||||||
|
Action,
|
||||||
|
//
|
||||||
|
Root as Card,
|
||||||
|
Content as CardContent,
|
||||||
|
Description as CardDescription,
|
||||||
|
Footer as CardFooter,
|
||||||
|
Header as CardHeader,
|
||||||
|
Title as CardTitle,
|
||||||
|
Action as CardAction,
|
||||||
|
};
|
||||||
16
src/lib/components/ui/tabs/index.ts
Normal file
16
src/lib/components/ui/tabs/index.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import Root from "./tabs.svelte";
|
||||||
|
import Content from "./tabs-content.svelte";
|
||||||
|
import List from "./tabs-list.svelte";
|
||||||
|
import Trigger from "./tabs-trigger.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
Content,
|
||||||
|
List,
|
||||||
|
Trigger,
|
||||||
|
//
|
||||||
|
Root as Tabs,
|
||||||
|
Content as TabsContent,
|
||||||
|
List as TabsList,
|
||||||
|
Trigger as TabsTrigger,
|
||||||
|
};
|
||||||
17
src/lib/components/ui/tabs/tabs-content.svelte
Normal file
17
src/lib/components/ui/tabs/tabs-content.svelte
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { Tabs as TabsPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: TabsPrimitive.ContentProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<TabsPrimitive.Content
|
||||||
|
bind:ref
|
||||||
|
data-slot="tabs-content"
|
||||||
|
class={cn("flex-1 outline-none", className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
20
src/lib/components/ui/tabs/tabs-list.svelte
Normal file
20
src/lib/components/ui/tabs/tabs-list.svelte
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { Tabs as TabsPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: TabsPrimitive.ListProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<TabsPrimitive.List
|
||||||
|
bind:ref
|
||||||
|
data-slot="tabs-list"
|
||||||
|
class={cn(
|
||||||
|
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
20
src/lib/components/ui/tabs/tabs-trigger.svelte
Normal file
20
src/lib/components/ui/tabs/tabs-trigger.svelte
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { Tabs as TabsPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: TabsPrimitive.TriggerProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<TabsPrimitive.Trigger
|
||||||
|
bind:ref
|
||||||
|
data-slot="tabs-trigger"
|
||||||
|
class={cn(
|
||||||
|
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
19
src/lib/components/ui/tabs/tabs.svelte
Normal file
19
src/lib/components/ui/tabs/tabs.svelte
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { Tabs as TabsPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
value = $bindable(""),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: TabsPrimitive.RootProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<TabsPrimitive.Root
|
||||||
|
bind:ref
|
||||||
|
bind:value
|
||||||
|
data-slot="tabs"
|
||||||
|
class={cn("flex flex-col gap-2", className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
|
|
@ -3,10 +3,12 @@ import { definePrefix, type Puuid } from "./puuid"
|
||||||
export const UserID = definePrefix("user");
|
export const UserID = definePrefix("user");
|
||||||
export const GroupID = definePrefix("group");
|
export const GroupID = definePrefix("group");
|
||||||
export const ServerID = definePrefix("srv");
|
export const ServerID = definePrefix("srv");
|
||||||
|
export const FriendRequestID = definePrefix("frq");
|
||||||
|
|
||||||
export type UserId = Puuid<"user">;
|
export type UserId = Puuid<"user">;
|
||||||
export type GroupId = Puuid<"group">;
|
export type GroupId = Puuid<"group">;
|
||||||
export type ServerId = Puuid<"srv">;
|
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,
|
OFFLINE: 1,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import type { RequestEvent } from '@sveltejs/kit';
|
import type { RequestEvent } from '@sveltejs/kit';
|
||||||
import { eq, inArray } from 'drizzle-orm';
|
import { eq, inArray, or } from 'drizzle-orm';
|
||||||
import { sha256 } from '@oslojs/crypto/sha2';
|
import { sha256 } from '@oslojs/crypto/sha2';
|
||||||
import { encodeBase64url, encodeHexLowerCase } from '@oslojs/encoding';
|
import { encodeBase64url, encodeHexLowerCase } from '@oslojs/encoding';
|
||||||
import { db } from '$lib/server/db';
|
import { db } from '$lib/server/db';
|
||||||
|
|
@ -94,8 +94,17 @@ export async function validateSessionToken(token: string) {
|
||||||
.where(inArray(table.group.id, (user.groups as string[])))
|
.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)))
|
||||||
|
|
||||||
return { session, user: {...user, servers, friends, groups: groups.map(z => { return { ...z, members: (z.members as string[]).length}})} };
|
|
||||||
|
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 type SessionValidationResult = Awaited<ReturnType<typeof validateSessionToken>>;
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,17 @@ export const channel = sqliteTable("channel", {
|
||||||
messages: text('messages', { mode: "json"}).default([]).notNull(),
|
messages: text('messages', { mode: "json"}).default([]).notNull(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const friendRequest = sqliteTable("friendRequest", {
|
||||||
|
id: text('id').primaryKey(),
|
||||||
|
fromUser: text('from_user').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 Session = typeof session.$inferSelect;
|
||||||
export type User = typeof user.$inferSelect;
|
export type User = typeof user.$inferSelect;
|
||||||
export type Group = typeof group.$inferSelect;
|
export type Group = typeof group.$inferSelect;
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,10 @@ import { getRequestEvent } from '$app/server';
|
||||||
import type { Actions, PageServerLoad } from './$types';
|
import type { Actions, PageServerLoad } from './$types';
|
||||||
import { db } from '$lib/server/db';
|
import { db } from '$lib/server/db';
|
||||||
import * as table from '$lib/server/db/schema';
|
import * as table from '$lib/server/db/schema';
|
||||||
import { ServerID } from '$lib';
|
import { FriendRequestID, ServerID } from '$lib';
|
||||||
import { eq } from 'drizzle-orm';
|
import { eq } from 'drizzle-orm';
|
||||||
|
import { and } from 'drizzle-orm';
|
||||||
|
import { User } from '$lib/server/db/schema';
|
||||||
export const load: PageServerLoad = async () => {
|
export const load: PageServerLoad = async () => {
|
||||||
const user = requireLogin();
|
const user = requireLogin();
|
||||||
return { user };
|
return { user };
|
||||||
|
|
@ -16,19 +17,98 @@ export const actions = {
|
||||||
addFriend: async ({ request, locals }) => {
|
addFriend: async ({ request, locals }) => {
|
||||||
const data = await request.formData();
|
const data = await request.formData();
|
||||||
const username = data.get('username');
|
const username = data.get('username');
|
||||||
|
const userId = data.get('userId');
|
||||||
|
|
||||||
|
if (username && !userId) {
|
||||||
if (typeof username !== 'string' || username.length < 3) {
|
if (typeof username !== 'string' || username.length < 3) {
|
||||||
return fail(400, { error: 'Invalid username' });
|
return fail(400, { error: 'Invalid username' });
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*await db.friendRequest.create({
|
let user: User[];
|
||||||
fromId: locals.user.id,
|
if(username) {
|
||||||
toUsername: username
|
user = await db.select().from(table.user).where(eq(table.user.username, username.toString())).limit(1);
|
||||||
});*/
|
} else if(userId) {
|
||||||
|
user = await db.select().from(table.user).where(eq(table.user.id, userId.toString())).limit(1);
|
||||||
|
} else {
|
||||||
|
return fail(400, { error: 'Missing username or userId' });
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if(user?.length == 0)
|
||||||
|
return fail(400, { error: 'User not found' });
|
||||||
|
|
||||||
|
const friendRequest = await db.select()
|
||||||
|
.from(table.friendRequest)
|
||||||
|
.where(and(eq(table.friendRequest.fromUser, user[0].id), eq(table.friendRequest.toUser, locals.user!.id)))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
// user has already sent a request to us
|
||||||
|
// means we want to accept it
|
||||||
|
//
|
||||||
|
if(friendRequest?.length != 0) {
|
||||||
|
await db.delete(table.friendRequest)
|
||||||
|
.where(and(eq(table.friendRequest.fromUser, user[0].id), eq(table.friendRequest.toUser, locals.user!.id)))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
// add other guy to us
|
||||||
|
await db.transaction(async (tx) => {
|
||||||
|
await tx.update(table.user)
|
||||||
|
.set({ friends: locals.user?.friends.map(z => z.id).concat(user[0].id) })
|
||||||
|
.where(eq(table.user.id, locals.user!.id));
|
||||||
|
|
||||||
|
await tx.update(table.user)
|
||||||
|
.set({ friends: (user[0].friends as string[]).concat(locals.user!.id) })
|
||||||
|
.where(eq(table.user.id, user[0].id));
|
||||||
|
});
|
||||||
|
|
||||||
|
return {success: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
// a request from us has already been sent to user
|
||||||
|
if(locals.user?.friendRequests.find(z => z.toUser == user[0].id && z.fromUser == locals.user!.id))
|
||||||
|
return fail(400, { error: 'Already sent request' });
|
||||||
|
|
||||||
|
await db.insert(table.friendRequest).values({
|
||||||
|
id: FriendRequestID.newV4(),
|
||||||
|
fromUser: locals.user!.id,
|
||||||
|
toUser: user[0].id,
|
||||||
|
});
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
},
|
},
|
||||||
|
cancelFriendRequest: async ({ request, locals }) => {
|
||||||
|
const data = await request.formData();
|
||||||
|
const requestId = data.get('requestId');
|
||||||
|
|
||||||
|
if (typeof requestId !== 'string') {
|
||||||
|
return fail(400, { error: 'Invalid request ID' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetch the friend request
|
||||||
|
const friendRequest = await db.select()
|
||||||
|
.from(table.friendRequest)
|
||||||
|
.where(eq(table.friendRequest.id, requestId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!friendRequest?.length) {
|
||||||
|
return fail(404, { error: 'Friend request not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const fr = friendRequest[0];
|
||||||
|
|
||||||
|
// only allow cancelling if it's related to current user
|
||||||
|
if (fr.fromUser !== locals.user!.id && fr.toUser !== locals.user!.id) {
|
||||||
|
return fail(403, { error: 'Not allowed' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete the request
|
||||||
|
await db.delete(table.friendRequest)
|
||||||
|
.where(eq(table.friendRequest.id, requestId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
},
|
||||||
createGroup: async ({ request, locals }) => {
|
createGroup: async ({ request, locals }) => {
|
||||||
const data = await request.formData();
|
const data = await request.formData();
|
||||||
const members = data.getAll('member');
|
const members = data.getAll('member');
|
||||||
|
|
@ -37,10 +117,7 @@ export const actions = {
|
||||||
return fail(400, { error: 'No members selected' });
|
return fail(400, { error: 'No members selected' });
|
||||||
}
|
}
|
||||||
|
|
||||||
await db.group.create({
|
console.log(data, members, locals)
|
||||||
ownerId: locals.user.id,
|
|
||||||
members: members as string[]
|
|
||||||
});
|
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
},
|
},
|
||||||
|
|
@ -53,7 +130,29 @@ export const actions = {
|
||||||
return fail(400, { error: 'Invalid invite' });
|
return fail(400, { error: 'Invalid invite' });
|
||||||
}
|
}
|
||||||
|
|
||||||
await db.server.joinByInvite(invite, locals.user.id);
|
const inv = await db.select().from(table.invite).where(eq(table.invite.code, invite)).limit(1);
|
||||||
|
|
||||||
|
if(inv?.length == 0)
|
||||||
|
return fail(400, { error: 'Invalid invite' });
|
||||||
|
|
||||||
|
const server = await db.select().from(table.server).where(eq(table.server.id, inv[0].serverId)).limit(1);
|
||||||
|
|
||||||
|
if(server?.length == 0)
|
||||||
|
return fail(400, { error: 'Invalid server' });
|
||||||
|
|
||||||
|
if(locals.user!.servers.some(z => z.id == server[0].id))
|
||||||
|
return fail(400, { error: 'Already in server' });
|
||||||
|
|
||||||
|
await db.transaction(async (tx) => {
|
||||||
|
await tx.update(table.user)
|
||||||
|
.set({servers: locals.user!.servers.map(z => z.id).concat([server[0].id])})
|
||||||
|
.where(eq(table.user.id, locals.user!.id));
|
||||||
|
|
||||||
|
await tx.update(table.server)
|
||||||
|
.set({members: (server[0].members as string[]).concat([locals.user!.id])})
|
||||||
|
.where(eq(table.server.id, server[0].id));
|
||||||
|
})
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -70,7 +169,7 @@ export const actions = {
|
||||||
.values({id: serverId, name, owner: locals.user!.id, members: [ locals.user!.id ]});
|
.values({id: serverId, name, owner: locals.user!.id, members: [ locals.user!.id ]});
|
||||||
|
|
||||||
await db.update(table.user)
|
await db.update(table.user)
|
||||||
.set({servers: (locals.user!.servers as string[]).concat([serverId])})
|
.set({servers: locals.user!.servers.map(z => z.id).concat([serverId])})
|
||||||
.where(eq(table.user.id, locals.user!.id));
|
.where(eq(table.user.id, locals.user!.id));
|
||||||
|
|
||||||
redirect(303, `/app`);
|
redirect(303, `/app`);
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,16 @@
|
||||||
import { Status, type OverviewData,
|
import { Status, type OverviewData,
|
||||||
GroupID, UserID, ServerID,
|
GroupID, UserID, ServerID,
|
||||||
type GroupId, type ServerId, type UserId,
|
type GroupId, type ServerId, type UserId,
|
||||||
type OverviewUser, type OverviewGroup, type OverviewServer } from "$lib";
|
type OverviewUser, type OverviewGroup, type OverviewServer,
|
||||||
|
type UserWithStatus} from "$lib";
|
||||||
import type { PageServerData } from './$types';
|
import type { PageServerData } from './$types';
|
||||||
import AppSidebar from "$lib/components/app-sidebar.svelte";
|
import AppSidebar from "$lib/components/app-sidebar.svelte";
|
||||||
import * as Sidebar from "$lib/components/ui/sidebar/index.js";
|
import * as Sidebar from "$lib/components/ui/sidebar/index.js";
|
||||||
|
import * as AlertDialog from "$lib/components/ui/alert-dialog/index.js";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
|
import type { ActionData } from './$types';
|
||||||
let { data }: { data: PageServerData } = $props();
|
let errorOpen = $state(true);
|
||||||
|
let { form, data }: { form: ActionData, data: PageServerData } = $props();
|
||||||
let currentPageID: (UserId|GroupId|ServerId)|null = $state(null);
|
let currentPageID: (UserId|GroupId|ServerId)|null = $state(null);
|
||||||
let currentPage: OverviewUser | OverviewGroup | OverviewServer | undefined = $state();
|
let currentPage: OverviewUser | OverviewGroup | OverviewServer | undefined = $state();
|
||||||
|
|
||||||
|
|
@ -19,7 +21,7 @@
|
||||||
servers: []
|
servers: []
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(data, overview_data)
|
console.log(form, data, overview_data)
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (currentPageID) {
|
if (currentPageID) {
|
||||||
|
|
@ -80,27 +82,44 @@
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
{#if form}
|
||||||
|
<AlertDialog.Root bind:open={errorOpen}>
|
||||||
|
<AlertDialog.Content>
|
||||||
|
<AlertDialog.Header>
|
||||||
|
<AlertDialog.Title>{form?.error ? "Ran into an error." : "Success!"}</AlertDialog.Title>
|
||||||
|
<AlertDialog.Description>
|
||||||
|
{form?.error || "Action completed succesfully."}
|
||||||
|
</AlertDialog.Description>
|
||||||
|
</AlertDialog.Header>
|
||||||
|
<AlertDialog.Footer>
|
||||||
|
<AlertDialog.Cancel>Close</AlertDialog.Cancel>
|
||||||
|
</AlertDialog.Footer>
|
||||||
|
</AlertDialog.Content>
|
||||||
|
</AlertDialog.Root>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
|
||||||
<Sidebar.Provider>
|
<Sidebar.Provider>
|
||||||
<AppSidebar bind:currentPage={currentPageID} data={overview_data} />
|
<AppSidebar bind:currentPage={currentPageID} user={data.user} data={overview_data} />
|
||||||
|
|
||||||
<Sidebar.Inset>
|
<Sidebar.Inset>
|
||||||
<header class="flex h-16 shrink-0 items-center gap-2 border-b px-4">
|
<header class="flex h-16 shrink-0 items-center gap-2 border-b px-4">
|
||||||
<Sidebar.Trigger class="-ms-1" />
|
<Sidebar.Trigger class="-ms-1" />
|
||||||
{#if currentPageID && currentPage}
|
{#if currentPageID && currentPage}
|
||||||
{#if ServerID.is(currentPageID)}
|
{#if ServerID.is(currentPageID)}
|
||||||
{@const server = (currentPage as Server)}
|
{@const server = (currentPage as OverviewServer)}
|
||||||
|
|
||||||
<img src={server!.image} alt={server!.name} class="size-6 rounded-full" />
|
<img src={server!.image} alt={server!.name} class="size-6 rounded-full" />
|
||||||
|
|
||||||
<h1>{server!.name}</h1>
|
<h1>{server!.name}</h1>
|
||||||
{:else if UserID.is(currentPageID)}
|
{:else if UserID.is(currentPageID)}
|
||||||
{@const friend = (currentPage as User)}
|
{@const friend = (currentPage as UserWithStatus)}
|
||||||
|
|
||||||
<img src={friend.image} alt={friend!.name} class="size-6 rounded-full" />
|
<img src={friend.image} alt={friend!.username} class="size-6 rounded-full" />
|
||||||
|
|
||||||
<h1>{friend!.name} [{friend.status == Status.ONLINE ? "Online!" : friend.status == Status.DND ? "DND" : friend.status == Status.OFFLINE ? "Offline" : "Unknown"}]</h1>
|
<h1>{friend!.username} [{friend.status == Status.ONLINE ? "Online!" : friend.status == Status.DND ? "DND" : friend.status == Status.OFFLINE ? "Offline" : "Unknown"}]</h1>
|
||||||
{:else if GroupID.is(currentPageID)}
|
{:else if GroupID.is(currentPageID)}
|
||||||
{@const group = (currentPage as Group)}
|
{@const group = (currentPage as OverviewGroup)}
|
||||||
|
|
||||||
<h1>{group!.name} ({group.members} member{group.members > 1 ? "s" : ""})</h1>
|
<h1>{group!.name} ({group.members} member{group.members > 1 ? "s" : ""})</h1>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue