mirror of https://github.com/TZGyn/shortener
added projects
parent
8a39b9b1df
commit
f06d78b4a8
@ -0,0 +1,8 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS "project" (
|
||||||
|
"id" serial PRIMARY KEY NOT NULL,
|
||||||
|
"uuid" uuid DEFAULT gen_random_uuid(),
|
||||||
|
"name" varchar(255) NOT NULL,
|
||||||
|
"user_id" integer NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "shortener" ADD COLUMN "project_id" integer;
|
||||||
@ -0,0 +1,228 @@
|
|||||||
|
{
|
||||||
|
"id": "9a3733db-b0ef-4744-b0bf-f69e9b03917c",
|
||||||
|
"prevId": "8f8ae49c-43bf-4e07-a30d-383e2b767524",
|
||||||
|
"version": "5",
|
||||||
|
"dialect": "pg",
|
||||||
|
"tables": {
|
||||||
|
"project": {
|
||||||
|
"name": "project",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"uuid": {
|
||||||
|
"name": "uuid",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
},
|
||||||
|
"session": {
|
||||||
|
"name": "session",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"token": {
|
||||||
|
"name": "token",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"expires": {
|
||||||
|
"name": "expires",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
},
|
||||||
|
"shortener": {
|
||||||
|
"name": "shortener",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"link": {
|
||||||
|
"name": "link",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"code": {
|
||||||
|
"name": "code",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"project_id": {
|
||||||
|
"name": "project_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"name": "user",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"uuid": {
|
||||||
|
"name": "uuid",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"username": {
|
||||||
|
"name": "username",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"name": "password",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"user_email_unique": {
|
||||||
|
"name": "user_email_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"email"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"visitor": {
|
||||||
|
"name": "visitor",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"shortener_id": {
|
||||||
|
"name": "shortener_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"country_code": {
|
||||||
|
"name": "country_code",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"country": {
|
||||||
|
"name": "country",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"city": {
|
||||||
|
"name": "city",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {},
|
||||||
|
"schemas": {},
|
||||||
|
"_meta": {
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {},
|
||||||
|
"columns": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
import { db } from '$lib/db'
|
||||||
|
import type { PageServerLoad } from './$types'
|
||||||
|
|
||||||
|
export const load = (async (event) => {
|
||||||
|
const user = event.locals.userObject
|
||||||
|
|
||||||
|
const projects = await db.query.project.findMany({
|
||||||
|
with: {
|
||||||
|
shortener: true,
|
||||||
|
},
|
||||||
|
where: (project, { eq }) => eq(project.userId, user.id),
|
||||||
|
})
|
||||||
|
|
||||||
|
return { projects }
|
||||||
|
}) satisfies PageServerLoad
|
||||||
@ -0,0 +1,109 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { PageData } from './$types'
|
||||||
|
import { Separator } from '$lib/components/ui/separator'
|
||||||
|
import { Button, buttonVariants } from '$lib/components/ui/button'
|
||||||
|
import * as Dialog from '$lib/components/ui/dialog'
|
||||||
|
import * as Card from '$lib/components/ui/card'
|
||||||
|
import * as DropdownMenu from '$lib/components/ui/dropdown-menu'
|
||||||
|
import { Input } from '$lib/components/ui/input'
|
||||||
|
import { Label } from '$lib/components/ui/label'
|
||||||
|
import {
|
||||||
|
ExternalLink,
|
||||||
|
Loader2,
|
||||||
|
MoreVertical,
|
||||||
|
PlusCircle,
|
||||||
|
} from 'lucide-svelte'
|
||||||
|
|
||||||
|
import { invalidateAll } from '$app/navigation'
|
||||||
|
|
||||||
|
export let data: PageData
|
||||||
|
|
||||||
|
let dialogOpen = false
|
||||||
|
let inputProjectName = ''
|
||||||
|
|
||||||
|
let isLoading = false
|
||||||
|
|
||||||
|
const addShortener = async () => {
|
||||||
|
isLoading = true
|
||||||
|
|
||||||
|
const response = await fetch('/api/project', {
|
||||||
|
method: 'post',
|
||||||
|
body: JSON.stringify({ name: inputProjectName }),
|
||||||
|
})
|
||||||
|
|
||||||
|
const responseData = await response.json()
|
||||||
|
|
||||||
|
isLoading = false
|
||||||
|
|
||||||
|
if (responseData.success) {
|
||||||
|
await invalidateAll()
|
||||||
|
dialogOpen = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex justify-between p-8">
|
||||||
|
<div class="text-4xl font-bold">Projects</div>
|
||||||
|
<Dialog.Root bind:open={dialogOpen}>
|
||||||
|
<Dialog.Trigger
|
||||||
|
class={buttonVariants({ variant: 'default' }) + 'flex gap-2'}>
|
||||||
|
<PlusCircle />
|
||||||
|
Add Project
|
||||||
|
</Dialog.Trigger>
|
||||||
|
<Dialog.Content class="sm:max-w-[425px]">
|
||||||
|
<Dialog.Header>
|
||||||
|
<Dialog.Title>Add Project</Dialog.Title>
|
||||||
|
<Dialog.Description>
|
||||||
|
Create A New Project Here. Click Add To Create.
|
||||||
|
</Dialog.Description>
|
||||||
|
</Dialog.Header>
|
||||||
|
<div class="grid gap-4 py-4">
|
||||||
|
<div class="grid grid-cols-4 items-center gap-4">
|
||||||
|
<Label class="text-right">Name</Label>
|
||||||
|
<Input
|
||||||
|
id="name"
|
||||||
|
bind:value={inputProjectName}
|
||||||
|
class="col-span-3" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Dialog.Footer>
|
||||||
|
<Button on:click={addShortener} class="flex gap-2">
|
||||||
|
{#if isLoading}
|
||||||
|
<Loader2 class="animate-spin" />
|
||||||
|
{/if}
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
|
</Dialog.Footer>
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Root>
|
||||||
|
</div>
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
{#if data.projects.length > 0}
|
||||||
|
<div class="flex flex-col gap-4 overflow-scroll p-4">
|
||||||
|
{#each data.projects as project}
|
||||||
|
<a href={'/projects/' + project.uuid}>
|
||||||
|
<Card.Root
|
||||||
|
class="hover:bg-secondary w-full max-w-[500px] hover:cursor-pointer">
|
||||||
|
<Card.Header>
|
||||||
|
<Card.Title class="flex items-center gap-2">
|
||||||
|
{project.name}
|
||||||
|
</Card.Title>
|
||||||
|
</Card.Header>
|
||||||
|
<Card.Content>
|
||||||
|
<div class="flex w-full justify-between">
|
||||||
|
<Button
|
||||||
|
class="bg-secondary flex h-8 items-center justify-center gap-1 rounded text-sm">
|
||||||
|
<ExternalLink size={20} />
|
||||||
|
{project.shortener.length}
|
||||||
|
Shorteners
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div>No Data</div>
|
||||||
|
{/if}
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
import { db } from '$lib/db'
|
||||||
|
import { redirect } from '@sveltejs/kit'
|
||||||
|
import type { PageServerLoad } from './$types'
|
||||||
|
|
||||||
|
export const load = (async (event) => {
|
||||||
|
const uuid = event.params.uuid
|
||||||
|
|
||||||
|
const user = event.locals.userObject
|
||||||
|
|
||||||
|
try {
|
||||||
|
const project = await db.query.project.findFirst({
|
||||||
|
where: (project, { eq }) => eq(project.uuid, uuid),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!project) {
|
||||||
|
throw redirect(303, '/projects')
|
||||||
|
}
|
||||||
|
|
||||||
|
const shorteners = await db.query.shortener.findMany({
|
||||||
|
with: {
|
||||||
|
visitor: true,
|
||||||
|
},
|
||||||
|
where: (shortener, { eq, and }) =>
|
||||||
|
and(
|
||||||
|
eq(shortener.userId, user.id),
|
||||||
|
eq(shortener.projectId, project.id),
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
|
return { project, shorteners }
|
||||||
|
} catch (error) {
|
||||||
|
throw redirect(303, '/projects')
|
||||||
|
}
|
||||||
|
}) satisfies PageServerLoad
|
||||||
@ -0,0 +1,200 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { PageData } from './$types'
|
||||||
|
import { Separator } from '$lib/components/ui/separator'
|
||||||
|
import { Button, buttonVariants } from '$lib/components/ui/button'
|
||||||
|
import * as Dialog from '$lib/components/ui/dialog'
|
||||||
|
import * as Card from '$lib/components/ui/card'
|
||||||
|
import * as DropdownMenu from '$lib/components/ui/dropdown-menu'
|
||||||
|
import { Input } from '$lib/components/ui/input'
|
||||||
|
import { Label } from '$lib/components/ui/label'
|
||||||
|
import {
|
||||||
|
BarChart,
|
||||||
|
ExternalLink,
|
||||||
|
Loader2,
|
||||||
|
MoreVertical,
|
||||||
|
PlusCircle,
|
||||||
|
} from 'lucide-svelte'
|
||||||
|
import { goto, invalidateAll } from '$app/navigation'
|
||||||
|
|
||||||
|
export let data: PageData
|
||||||
|
|
||||||
|
let dialogOpen = false
|
||||||
|
let inputLink = ''
|
||||||
|
let isLoading = false
|
||||||
|
|
||||||
|
const addShortener = async () => {
|
||||||
|
isLoading = true
|
||||||
|
|
||||||
|
const response = await fetch('/api/shortener', {
|
||||||
|
method: 'post',
|
||||||
|
body: JSON.stringify({ link: inputLink }),
|
||||||
|
})
|
||||||
|
|
||||||
|
const responseData = await response.json()
|
||||||
|
|
||||||
|
isLoading = false
|
||||||
|
|
||||||
|
if (responseData.success) {
|
||||||
|
await invalidateAll()
|
||||||
|
dialogOpen = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let editDialogOpen = false
|
||||||
|
let editShortenerCode = ''
|
||||||
|
let editShortenerLink = ''
|
||||||
|
let isEditLoading = false
|
||||||
|
|
||||||
|
const openEditDialog = (code: string, link: string) => {
|
||||||
|
editDialogOpen = true
|
||||||
|
editShortenerCode = code
|
||||||
|
editShortenerLink = link
|
||||||
|
}
|
||||||
|
|
||||||
|
const editShortener = async (code: string, link: string) => {
|
||||||
|
isEditLoading = true
|
||||||
|
await fetch(`/api/shortener/${code}`, {
|
||||||
|
method: 'put',
|
||||||
|
body: JSON.stringify({
|
||||||
|
link,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
await invalidateAll()
|
||||||
|
isEditLoading = false
|
||||||
|
editDialogOpen = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteShortener = async (code: string) => {
|
||||||
|
await fetch(`/api/shortener/${code}`, {
|
||||||
|
method: 'delete',
|
||||||
|
})
|
||||||
|
await invalidateAll()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex justify-between p-8">
|
||||||
|
<div class="text-4xl font-bold">Links</div>
|
||||||
|
<Dialog.Root bind:open={dialogOpen}>
|
||||||
|
<Dialog.Trigger
|
||||||
|
class={buttonVariants({ variant: 'default' }) + 'flex gap-2'}>
|
||||||
|
<PlusCircle />
|
||||||
|
Add Shortner
|
||||||
|
</Dialog.Trigger>
|
||||||
|
<Dialog.Content class="sm:max-w-[425px]">
|
||||||
|
<Dialog.Header>
|
||||||
|
<Dialog.Title>Add Shortener</Dialog.Title>
|
||||||
|
<Dialog.Description>
|
||||||
|
Create A New Shortner Here. Click Add To Save.
|
||||||
|
</Dialog.Description>
|
||||||
|
</Dialog.Header>
|
||||||
|
<div class="grid gap-4 py-4">
|
||||||
|
<div class="grid grid-cols-4 items-center gap-4">
|
||||||
|
<Label class="text-right">Link</Label>
|
||||||
|
<Input
|
||||||
|
id="name"
|
||||||
|
bind:value={inputLink}
|
||||||
|
class="col-span-3" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Dialog.Footer>
|
||||||
|
<Button on:click={addShortener} class="flex gap-2">
|
||||||
|
{#if isLoading}
|
||||||
|
<Loader2 class="animate-spin" />
|
||||||
|
{/if}
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
|
</Dialog.Footer>
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Root>
|
||||||
|
</div>
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
{#if data.shorteners.length > 0}
|
||||||
|
<div class="flex flex-col gap-4 overflow-scroll p-4">
|
||||||
|
{#each data.shorteners as shortener}
|
||||||
|
<Card.Root class="w-full max-w-[500px]">
|
||||||
|
<Card.Header>
|
||||||
|
<Card.Title class="flex items-center gap-2">
|
||||||
|
<a
|
||||||
|
href={'https://' +
|
||||||
|
data.shortener_url +
|
||||||
|
'/' +
|
||||||
|
shortener.code}
|
||||||
|
target="_blank"
|
||||||
|
class="hover:underline">
|
||||||
|
{data.shortener_url + '/' + shortener.code}
|
||||||
|
</a>
|
||||||
|
<ExternalLink size={16} />
|
||||||
|
</Card.Title>
|
||||||
|
<Card.Description>{shortener.link}</Card.Description>
|
||||||
|
</Card.Header>
|
||||||
|
<Card.Content>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<Button
|
||||||
|
class="bg-secondary flex h-8 items-center justify-center gap-1 rounded text-sm"
|
||||||
|
on:click={() => goto(`/links/${shortener.code}`)}>
|
||||||
|
<BarChart size={20} />
|
||||||
|
<div>
|
||||||
|
{shortener.visitor.length} visits
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<DropdownMenu.Root>
|
||||||
|
<DropdownMenu.Trigger>
|
||||||
|
<MoreVertical />
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
<DropdownMenu.Content>
|
||||||
|
<DropdownMenu.Group>
|
||||||
|
<DropdownMenu.Item
|
||||||
|
on:click={() =>
|
||||||
|
openEditDialog(shortener.code, shortener.link)}>
|
||||||
|
Edit
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item
|
||||||
|
on:click={() => deleteShortener(shortener.code)}
|
||||||
|
class="text-destructive data-[highlighted]:bg-destructive">
|
||||||
|
Delete
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</DropdownMenu.Group>
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Root>
|
||||||
|
</div>
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div>No Data</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<Dialog.Root bind:open={editDialogOpen}>
|
||||||
|
<Dialog.Content class="sm:max-w-[425px]">
|
||||||
|
<Dialog.Header>
|
||||||
|
<Dialog.Title>Edit Shortener {editShortenerCode}</Dialog.Title>
|
||||||
|
<Dialog.Description>
|
||||||
|
Edit Shortner Here. Click Save To Update.
|
||||||
|
</Dialog.Description>
|
||||||
|
</Dialog.Header>
|
||||||
|
<div class="grid gap-4 py-4">
|
||||||
|
<div class="grid grid-cols-4 items-center gap-4">
|
||||||
|
<Label class="text-right">Link</Label>
|
||||||
|
<Input
|
||||||
|
id="name"
|
||||||
|
bind:value={editShortenerLink}
|
||||||
|
class="col-span-3" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Dialog.Footer>
|
||||||
|
<Button
|
||||||
|
on:click={() =>
|
||||||
|
editShortener(editShortenerCode, editShortenerLink)}
|
||||||
|
class="flex gap-2">
|
||||||
|
{#if isEditLoading}
|
||||||
|
<Loader2 class="animate-spin" />
|
||||||
|
{/if}
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</Dialog.Footer>
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Root>
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
import { z } from 'zod'
|
||||||
|
import type { RequestHandler } from './$types'
|
||||||
|
import { db } from '$lib/db'
|
||||||
|
import { project } from '$lib/db/schema'
|
||||||
|
|
||||||
|
export const GET: RequestHandler = async () => {
|
||||||
|
return new Response()
|
||||||
|
}
|
||||||
|
|
||||||
|
const projectInsertSchema = z.object({
|
||||||
|
name: z.string(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const POST: RequestHandler = async (event) => {
|
||||||
|
const body = await event.request.json()
|
||||||
|
|
||||||
|
const projectInsert = projectInsertSchema.safeParse(body)
|
||||||
|
|
||||||
|
if (!projectInsert.success) {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
message: 'Invalid Data',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = event.locals.userObject
|
||||||
|
|
||||||
|
await db.insert(project).values({
|
||||||
|
name: projectInsert.data.name,
|
||||||
|
userId: user.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
return new Response(JSON.stringify({ success: true }))
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue