groups api route

This commit is contained in:
Omer Sabic 2024-03-12 23:07:07 +01:00
parent 3b1e627356
commit d62cc6f6a4
21 changed files with 488 additions and 15 deletions

60
drizzle/schema.ts Normal file
View File

@ -0,0 +1,60 @@
import { pgTable, foreignKey, unique, uuid, text, boolean, date } from "drizzle-orm/pg-core"
import { sql } from "drizzle-orm"
export const groups = pgTable("groups", {
id: uuid("id").defaultRandom().primaryKey().notNull(),
userId: uuid("user_id").references(() => users.id),
email: text("email").notNull(),
name: text("name").notNull(),
},
(table) => {
return {
groupsEmailUnique: unique("groups_email_unique").on(table.email),
}
});
export const sources = pgTable("sources", {
id: uuid("id").defaultRandom().primaryKey().notNull(),
groupId: uuid("group_id").notNull().references(() => groups.id),
name: text("name").notNull(),
isConfirmed: boolean("is_confirmed").default(false).notNull(),
});
export const letters = pgTable("letters", {
id: uuid("id").defaultRandom().primaryKey().notNull(),
sender: text("sender").notNull(),
content: text("content").notNull(),
groupId: uuid("group_id").notNull().references(() => groups.id),
dateCreated: date("date_created").defaultNow().notNull(),
senderEmail: text("sender_email").notNull(),
subject: text("subject").notNull(),
});
export const sessions = pgTable("sessions", {
id: uuid("id").defaultRandom().primaryKey().notNull(),
userId: uuid("user_id").notNull(),
dateCreated: date("date_created").defaultNow().notNull(),
});
export const users = pgTable("users", {
id: uuid("id").defaultRandom().primaryKey().notNull(),
name: text("name").notNull(),
email: text("email").notNull(),
hashedPassword: text("hashed_password").notNull(),
dateCreated: date("date_created").defaultNow().notNull(),
},
(table) => {
return {
usersEmailUnique: unique("users_email_unique").on(table.email),
}
});
export const pods = pgTable("pods", {
id: uuid("id").defaultRandom().primaryKey().notNull(),
userId: uuid("user_id").references(() => users.id),
script: text("script").notNull(),
dateCreated: date("date_created").defaultNow().notNull(),
groupId: uuid("group_id").references(() => groups.id),
});

76
package-lock.json generated
View File

@ -15,10 +15,13 @@
"drizzle-orm": "^0.30.1",
"formsnap": "^0.5.1",
"lucide-svelte": "^0.354.0",
"mode-watcher": "^0.2.2",
"pg": "^8.11.3",
"svelte-sonner": "^0.3.19",
"sveltekit-superforms": "^2.8.1",
"tailwind-merge": "^2.2.1",
"tailwind-variants": "^0.2.0",
"vaul-svelte": "^0.3.0",
"zod": "^3.22.4"
},
"devDependencies": {
@ -3186,6 +3189,14 @@
"mkdirp": "bin/cmd.js"
}
},
"node_modules/mode-watcher": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/mode-watcher/-/mode-watcher-0.2.2.tgz",
"integrity": "sha512-QjkHQL9pXrr7Vb0P3WbOWAF8mv1Q6jEwUZ5GUyCnI9eEoXH234zuaOGChUF7ZQtjxwtmXDzKFSW/36TvLDg1/A==",
"peerDependencies": {
"svelte": "^4.0.0"
}
},
"node_modules/mri": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
@ -4474,6 +4485,14 @@
}
}
},
"node_modules/svelte-sonner": {
"version": "0.3.19",
"resolved": "https://registry.npmjs.org/svelte-sonner/-/svelte-sonner-0.3.19.tgz",
"integrity": "sha512-jpPOgLtHwRaB6Vqo2dUQMv15/yUV/BQWTjKpEqQ11uqRSHKjAYUKZyGrHB2cQsGmyjR0JUzBD58btpgNqINQ/Q==",
"peerDependencies": {
"svelte": ">=3 <5"
}
},
"node_modules/sveltekit-superforms": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/sveltekit-superforms/-/sveltekit-superforms-2.8.1.tgz",
@ -4896,6 +4915,63 @@
"node": ">= 0.10"
}
},
"node_modules/vaul-svelte": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/vaul-svelte/-/vaul-svelte-0.3.0.tgz",
"integrity": "sha512-+PBfKDWl+xfloe8Tm1G8x3TqbCiUWoyUedU2WC5iE3v6LOYPKo8FyEtzNC5ZqFVVnUKSKNg+4Fi73nuzMkT7JA==",
"dependencies": {
"bits-ui": "^0.16.0"
},
"peerDependencies": {
"svelte": "^4.0.0"
}
},
"node_modules/vaul-svelte/node_modules/@melt-ui/svelte": {
"version": "0.68.0",
"resolved": "https://registry.npmjs.org/@melt-ui/svelte/-/svelte-0.68.0.tgz",
"integrity": "sha512-/QvA98hnYEodZtHJ71+ocum/WWp30hVNt3F8uiZKnNYwZDaiQYjlyR9AaGKYcZLCe6R68op1mfCzc0kTzJilyA==",
"dependencies": {
"@floating-ui/core": "^1.3.1",
"@floating-ui/dom": "^1.4.5",
"@internationalized/date": "^3.5.0",
"dequal": "^2.0.3",
"focus-trap": "^7.5.2",
"nanoid": "^5.0.4"
},
"peerDependencies": {
"svelte": ">=3 <5"
}
},
"node_modules/vaul-svelte/node_modules/bits-ui": {
"version": "0.16.0",
"resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-0.16.0.tgz",
"integrity": "sha512-HEkuVDyUG9dTWtKujKpdDsGOe9GRmuYOEF9yGbjVwNazxMQDQa9deUX8vM3ofGBWaJgr1cEu88p38kP5Z5gQ8w==",
"dependencies": {
"@internationalized/date": "^3.5.1",
"@melt-ui/svelte": "0.68.0",
"nanoid": "^5.0.4"
},
"peerDependencies": {
"svelte": "^4.0.0"
}
},
"node_modules/vaul-svelte/node_modules/nanoid": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.6.tgz",
"integrity": "sha512-rRq0eMHoGZxlvaFOUdK1Ev83Bd1IgzzR+WJ3IbDJ7QOSdAxYjlurSPqFs9s4lJg29RT6nPwizFtJhQS6V5xgiA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"bin": {
"nanoid": "bin/nanoid.js"
},
"engines": {
"node": "^18 || >=20"
}
},
"node_modules/vite": {
"version": "5.1.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.1.5.tgz",

View File

@ -39,10 +39,13 @@
"drizzle-orm": "^0.30.1",
"formsnap": "^0.5.1",
"lucide-svelte": "^0.354.0",
"mode-watcher": "^0.2.2",
"pg": "^8.11.3",
"svelte-sonner": "^0.3.19",
"sveltekit-superforms": "^2.8.1",
"tailwind-merge": "^2.2.1",
"tailwind-variants": "^0.2.0",
"vaul-svelte": "^0.3.0",
"zod": "^3.22.4"
}
}

View File

@ -0,0 +1,57 @@
<script>
import { PlusIcon } from 'lucide-svelte';
import { Button } from '../ui/button';
import * as Drawer from '../ui/drawer';
import { Input } from '../ui/input';
import { toast } from 'svelte-sonner';
let group_name = '';
async function createGroup() {
if (!group_name) return;
const groups = await (
await fetch('/api/groups', {
method: 'POST',
body: JSON.stringify({
name: group_name
})
})
).json();
if (!groups.success) return toast.error('Problem with creating group.');
return toast.success('Group created.');
}
</script>
<Drawer.Root>
<Drawer.Trigger asChild let:builder>
<Button builders={[builder]} class="w-full rounded" variant="outline">
<div class="flex flex-row justify-center items-center">
<div class="items-center justify-center rounded-lg p-2 pl-0">
<PlusIcon class="h-4 w-4" />
</div>
<div>
<h3 class="text-md">Create Group</h3>
</div>
</div>
</Button>
</Drawer.Trigger>
<Drawer.Content>
<div class="mx-auto w-full max-w-sm">
<Drawer.Header>
<Drawer.Title>Group title</Drawer.Title>
<Drawer.Description>Set a memorable name for your group.</Drawer.Description>
</Drawer.Header>
<div>
<Input type="text" bind:value={group_name} class="bg-primary" />
</div>
<Drawer.Footer>
<Button on:click={createGroup}>Submit</Button>
<Drawer.Close asChild let:builder>
<Button builders={[builder]} variant="outline">Cancel</Button>
</Drawer.Close>
</Drawer.Footer>
</div>
</Drawer.Content>
</Drawer.Root>

View File

@ -0,0 +1,24 @@
<script lang="ts">
import { Drawer as DrawerPrimitive } from "vaul-svelte";
import DrawerOverlay from "./drawer-overlay.svelte";
import { cn } from "$lib/utils.js.js";
type $$Props = DrawerPrimitive.ContentProps;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<DrawerPrimitive.Portal>
<DrawerOverlay />
<DrawerPrimitive.Content
class={cn(
"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border border-black bg-secondary",
className
)}
{...$$restProps}
>
<div class="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
<slot />
</DrawerPrimitive.Content>
</DrawerPrimitive.Portal>

View File

@ -0,0 +1,18 @@
<script lang="ts">
import { Drawer as DrawerPrimitive } from "vaul-svelte";
import { cn } from "$lib/utils.js.js";
type $$Props = DrawerPrimitive.DescriptionProps;
export let el: $$Props["el"] = undefined;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<DrawerPrimitive.Description
bind:el
class={cn("text-sm text-muted-foreground", className)}
{...$$restProps}
>
<slot />
</DrawerPrimitive.Description>

View File

@ -0,0 +1,16 @@
<script lang="ts">
import { cn } from "$lib/utils.js.js";
import type { HTMLAttributes } from "svelte/elements";
type $$Props = HTMLAttributes<HTMLDivElement> & {
el?: HTMLDivElement;
};
export let el: $$Props["el"] = undefined;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<div bind:this={el} class={cn("mt-auto flex flex-col gap-2 p-4", className)} {...$$restProps}>
<slot />
</div>

View File

@ -0,0 +1,19 @@
<script lang="ts">
import { cn } from "$lib/utils.js.js";
import type { HTMLAttributes } from "svelte/elements";
type $$Props = HTMLAttributes<HTMLDivElement> & {
el?: HTMLDivElement;
};
export let el: $$Props["el"] = undefined;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<div
bind:this={el}
class={cn("grid gap-1.5 p-4 text-center sm:text-left", className)}
{...$$restProps}
>
<slot />
</div>

View File

@ -0,0 +1,12 @@
<script lang="ts">
import { Drawer as DrawerPrimitive } from "vaul-svelte";
type $$Props = DrawerPrimitive.Props;
export let shouldScaleBackground: $$Props["shouldScaleBackground"] = true;
export let open: $$Props["open"] = false;
export let activeSnapPoint: $$Props["activeSnapPoint"] = undefined;
</script>
<DrawerPrimitive.NestedRoot {shouldScaleBackground} bind:open bind:activeSnapPoint {...$$restProps}>
<slot />
</DrawerPrimitive.NestedRoot>

View File

@ -0,0 +1,18 @@
<script lang="ts">
import { Drawer as DrawerPrimitive } from "vaul-svelte";
import { cn } from "$lib/utils.js.js";
type $$Props = DrawerPrimitive.OverlayProps;
export let el: $$Props["el"] = undefined;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<DrawerPrimitive.Overlay
bind:el
class={cn("fixed inset-0 z-50 bg-black/80", className)}
{...$$restProps}
>
<slot />
</DrawerPrimitive.Overlay>

View File

@ -0,0 +1,18 @@
<script lang="ts">
import { Drawer as DrawerPrimitive } from "vaul-svelte";
import { cn } from "$lib/utils.js.js";
type $$Props = DrawerPrimitive.TitleProps;
export let el: $$Props["el"] = undefined;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<DrawerPrimitive.Title
bind:el
class={cn("text-lg font-semibold leading-none tracking-tight", className)}
{...$$restProps}
>
<slot />
</DrawerPrimitive.Title>

View File

@ -0,0 +1,12 @@
<script lang="ts">
import { Drawer as DrawerPrimitive } from "vaul-svelte";
type $$Props = DrawerPrimitive.Props;
export let shouldScaleBackground: $$Props["shouldScaleBackground"] = true;
export let open: $$Props["open"] = false;
export let activeSnapPoint: $$Props["activeSnapPoint"] = undefined;
</script>
<DrawerPrimitive.Root {shouldScaleBackground} bind:open bind:activeSnapPoint {...$$restProps}>
<slot />
</DrawerPrimitive.Root>

View File

@ -0,0 +1,41 @@
import { Drawer as DrawerPrimitive } from "vaul-svelte";
import Root from "./drawer.svelte";
import Content from "./drawer-content.svelte";
import Description from "./drawer-description.svelte";
import Overlay from "./drawer-overlay.svelte";
import Footer from "./drawer-footer.svelte";
import Header from "./drawer-header.svelte";
import Title from "./drawer-title.svelte";
import NestedRoot from "./drawer-nested.svelte";
const Trigger = DrawerPrimitive.Trigger;
const Portal = DrawerPrimitive.Portal;
const Close = DrawerPrimitive.Close;
export {
Root,
NestedRoot,
Content,
Description,
Overlay,
Footer,
Header,
Title,
Trigger,
Portal,
Close,
//
Root as Drawer,
NestedRoot as DrawerNestedRoot,
Content as DrawerContent,
Description as DrawerDescription,
Overlay as DrawerOverlay,
Footer as DrawerFooter,
Header as DrawerHeader,
Title as DrawerTitle,
Trigger as DrawerTrigger,
Portal as DrawerPortal,
Close as DrawerClose,
};

View File

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

View File

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

View File

@ -1,4 +1,4 @@
import { date, pgTable, serial, text, uuid, varchar } from 'drizzle-orm/pg-core';
import { boolean, date, pgTable, serial, text, uuid, varchar } from 'drizzle-orm/pg-core';
import { drizzle } from 'drizzle-orm/node-postgres';
export const usersTable = pgTable('users', {
@ -13,6 +13,7 @@ export const podsTable = pgTable('pods', {
id: uuid('id').primaryKey().defaultRandom(),
user_id: uuid('user_id').references(() => usersTable.id),
script: text('script').notNull(),
groupId: uuid("group_id").references(() => groupsTable.id),
date_created: date('date_created').notNull().defaultNow()
});
@ -20,4 +21,28 @@ export const sessionsTable = pgTable('sessions', {
id: uuid('id').primaryKey().defaultRandom(),
user_id: uuid('user_id').notNull(),
date_created: date('date_created').notNull().defaultNow()
})
})
export const groupsTable = pgTable("groups", {
id: uuid("id").defaultRandom().primaryKey().notNull(),
userId: uuid("user_id").references(() => usersTable.id),
email: text("email").notNull().unique(),
name: text("name").notNull(),
});
export const sourcesTable = pgTable("sources", {
id: uuid("id").defaultRandom().primaryKey().notNull(),
groupId: uuid("group_id").notNull().references(() => groupsTable.id),
name: text("name").notNull(),
isConfirmed: boolean("is_confirmed").default(false).notNull(),
});
export const lettersTable = pgTable("letters", {
id: uuid("id").defaultRandom().primaryKey().notNull(),
sender: text("sender").notNull(),
content: text("content").notNull(),
groupId: uuid("group_id").notNull().references(() => groupsTable.id),
dateCreated: date("date_created").defaultNow().notNull(),
senderEmail: text("sender_email").notNull(),
subject: text("subject").notNull(),
});

View File

@ -12,7 +12,10 @@ export async function createSession(user_id: string) {
}
export async function validateSession(session_id: string) {
const session = await db.select().from(sessionsTable).where(eq(sessionsTable.id, session_id)).leftJoin(usersTable, eq(usersTable.id, sessionsTable.user_id));
return session[0].users;
try {
const session = await db.select().from(sessionsTable).where(eq(sessionsTable.id, session_id)).leftJoin(usersTable, eq(usersTable.id, sessionsTable.user_id));
return session[0].users;
} catch (e) {
return false;
}
}

View File

@ -1,7 +1,10 @@
<script>
import '../app.pcss';
import { Toaster } from '$lib/components/ui/sonner';
</script>
<Toaster />
<div class="max-w-xl mx-auto p-8 h-screen relative">
<slot />
</div>

View File

@ -4,23 +4,23 @@
import { Button } from '$lib/components/ui/button';
import Player from '$lib/components/organisms/player.svelte';
import { onMount } from 'svelte';
import { PlayIcon, Loader, Plus, MailIcon } from 'lucide-svelte';
import { PlayIcon, Loader, Plus, MailIcon, PlusIcon } from 'lucide-svelte';
import CreateGroup from '$lib/components/organisms/create-group.svelte';
let script = "";
let opened = false;
let pods = [];
let groups = [];
let pods = []
onMount(async () => {
let res = await (await fetch("/")).json();
console.log(res);
script = res.script;
res = await (await fetch("/api/pods")).json();
console.log(res);
pods = res.pods;
res = await (await fetch("/api/groups")).json();
groups = res.groups;
});
</script>
{#if opened}
@ -41,16 +41,16 @@
</Button>
{/each}
<Label>Your Newsletters</Label>
<Label>Your Groups</Label>
<div class="overflow-x-scroll scrollbars-hidden">
{#each pods as pod}
<CreateGroup />
{#each groups as group}
<Button class="mt-4 flex w-full flex-row justify-start rounded text-left" variant="secondary">
<div class="items-center justify-center rounded-lg p-2 pl-0">
<MailIcon class="h-4 w-4" />
</div>
<div>
<h3 class="text-md">{pod.title}</h3>
<p class="text-sm text-muted-foreground">{new Date(pod.date).toLocaleDateString('de')}</p>
<h3 class="text-md">{group.name}</h3>
</div>
</Button>
{/each}

View File

@ -0,0 +1,46 @@
import { db, groupsTable } from "$lib/db"
import { generateId } from "$lib/utils/auth.utils";
import { json } from "@sveltejs/kit";
import { eq } from "drizzle-orm";
/**
* @type {import("@sveltejs/kit").RequestHandler}
*/
export async function GET(event) {
console.log(event.locals.user.id)
const groups = await db.select().from(groupsTable).where(eq(groupsTable.userId, event.locals.user.id));
return new Response(JSON.stringify({
success: true,
groups
}));
}
/**
* @type {import("@sveltejs/kit").RequestHandler}
*/
export async function POST(event) {
const body = await event.request.json();
if (!body.name) return json({
success: false
});
const values = {
name: body.name,
email: `${generateId(7)}@smtp.omersabic.com`,
userId: event.locals.user.id
};
const groups = await db.insert(groupsTable).values(values).returning();
console.log(groups);
if (groups.length === 0) return json({
succcess: false
});
return json({
success: true,
group: groups[0]
});
}

View File

@ -7,6 +7,7 @@ import bcrypt from "bcrypt";
import * as authService from "$lib/services/auth.server";
import { eq } from "drizzle-orm";
import { hashPassword, validatePassword } from "$lib/utils/auth.utils";
/**
* @type {import("./$types").PageServerLoad}
*/