diff --git a/frontend/.env.example b/frontend/.env.example index 7cdbf76..77de6e2 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -32,4 +32,13 @@ PRIVATE_GOOGLE_CLIENT_SECRET= # Stripe PRIVATE_STRIPE_SECRET_KEY= PRIVATE_STRIPE_WEBHOOK_SECRET= -PRIVATE_PRO_PLAN_PRICE_ID= \ No newline at end of file +PRIVATE_PRO_PLAN_PRICE_ID= + +# Tigris/S3 +PRIVATE_AWS_WEBHOOK_TOKEN= +PRIVATE_AWS_BUCKET_NAME= +PRIVATE_AWS_ACCESS_KEY_ID= +PRIVATE_AWS_SECRET_ACCESS_KEY= +PRIVATE_AWS_ENDPOINT_URL_S3= +PRIVATE_AWS_ENDPOINT_URL_IAM= +PRIVATE_AWS_REGION=auto \ No newline at end of file diff --git a/frontend/bun.lockb b/frontend/bun.lockb index e232ceb..b928e42 100755 Binary files a/frontend/bun.lockb and b/frontend/bun.lockb differ diff --git a/frontend/drizzle/0028_hard_red_shift.sql b/frontend/drizzle/0028_hard_red_shift.sql new file mode 100644 index 0000000..634e656 --- /dev/null +++ b/frontend/drizzle/0028_hard_red_shift.sql @@ -0,0 +1,15 @@ +CREATE TABLE IF NOT EXISTS "file" ( + "id" text PRIMARY KEY NOT NULL, + "user_id" text NOT NULL, + "project_id" text, + "key" text NOT NULL, + "name" text NOT NULL, + "size" bigint DEFAULT 0 NOT NULL, + "etag" text NOT NULL, + "created_at_epoch" bigint NOT NULL, + "updated_at_epoch" bigint NOT NULL +); +--> statement-breakpoint +ALTER TABLE "shortener" ADD COLUMN "is_file_upload" boolean DEFAULT false NOT NULL;--> statement-breakpoint +ALTER TABLE "shortener" ADD COLUMN "file_path" text;--> statement-breakpoint +ALTER TABLE "user" ADD COLUMN "file_storage_usage_in_byte" bigint DEFAULT 0 NOT NULL; \ No newline at end of file diff --git a/frontend/drizzle/meta/0028_snapshot.json b/frontend/drizzle/meta/0028_snapshot.json new file mode 100644 index 0000000..41d70a7 --- /dev/null +++ b/frontend/drizzle/meta/0028_snapshot.json @@ -0,0 +1,542 @@ +{ + "id": "ce042384-c48f-4a7b-b067-8808e73834d6", + "prevId": "c91acbf2-ca0a-4726-b2b2-70fd1f8bd9ff", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.email_verification_token": { + "name": "email_verification_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.file": { + "name": "file", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "etag": { + "name": "etag", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at_epoch": { + "name": "created_at_epoch", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "updated_at_epoch": { + "name": "updated_at_epoch", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.project": { + "name": "project", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "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": "text", + "primaryKey": false, + "notNull": true + }, + "qr_background": { + "name": "qr_background", + "type": "varchar(7)", + "primaryKey": false, + "notNull": true, + "default": "'#ffffff'" + }, + "qr_foreground": { + "name": "qr_foreground", + "type": "varchar(7)", + "primaryKey": false, + "notNull": true, + "default": "'#000000'" + }, + "domain_status": { + "name": "domain_status", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'verified'" + }, + "enable_custom_domain": { + "name": "enable_custom_domain", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "custom_ip": { + "name": "custom_ip", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "custom_domain_id": { + "name": "custom_domain_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "custom_domain": { + "name": "custom_domain", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "qr_corner_square_style": { + "name": "qr_corner_square_style", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'square'" + }, + "qr_dot_style": { + "name": "qr_dot_style", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'square'" + }, + "qr_image_base64": { + "name": "qr_image_base64", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.shortener": { + "name": "shortener", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "link": { + "name": "link", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "ios": { + "name": "ios", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "ios_link": { + "name": "ios_link", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "android": { + "name": "android", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "android_link": { + "name": "android_link", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "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": "text", + "primaryKey": false, + "notNull": true + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_file_upload": { + "name": "is_file_upload", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "file_path": { + "name": "file_path", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "uuid": { + "name": "uuid", + "type": "uuid", + "primaryKey": false, + "notNull": false, + "default": "gen_random_uuid()" + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "google_id": { + "name": "google_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "plan": { + "name": "plan", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'free'" + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "qr_background": { + "name": "qr_background", + "type": "varchar(7)", + "primaryKey": false, + "notNull": true, + "default": "'#fff'" + }, + "qr_foreground": { + "name": "qr_foreground", + "type": "varchar(7)", + "primaryKey": false, + "notNull": true, + "default": "'#000'" + }, + "qr_corner_square_style": { + "name": "qr_corner_square_style", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'square'" + }, + "qr_dot_style": { + "name": "qr_dot_style", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'square'" + }, + "qr_image_base64": { + "name": "qr_image_base64", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "file_storage_usage_in_byte": { + "name": "file_storage_usage_in_byte", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + } + }, + "public.visitor": { + "name": "visitor", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "shortener_id": { + "name": "shortener_id", + "type": "text", + "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 + }, + "device_type": { + "name": "device_type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "device_vendor": { + "name": "device_vendor", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "os": { + "name": "os", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "browser": { + "name": "browser", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "referer": { + "name": "referer", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "''" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/frontend/drizzle/meta/_journal.json b/frontend/drizzle/meta/_journal.json index 6f3235e..c8d597b 100644 --- a/frontend/drizzle/meta/_journal.json +++ b/frontend/drizzle/meta/_journal.json @@ -197,6 +197,13 @@ "when": 1726769139213, "tag": "0027_funny_wasp", "breakpoints": true + }, + { + "idx": 28, + "version": "7", + "when": 1734847628427, + "tag": "0028_hard_red_shift", + "breakpoints": true } ] } \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index f4ae8fc..3b1ab8e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -40,6 +40,8 @@ }, "type": "module", "dependencies": { + "@aws-sdk/client-s3": "^3.705.0", + "@aws-sdk/s3-request-presigner": "^3.705.0", "@lucia-auth/adapter-drizzle": "^1.0.7", "@prgm/sveltekit-progress-bar": "^2.0.0", "@stripe/stripe-js": "^4.3.0", @@ -62,6 +64,7 @@ "qr-code-styling": "^1.6.0-rc.1", "resend": "^3.4.0", "stripe": "^16.8.0", + "svelte-file-dropzone": "^2.0.9", "svelte-sonner": "^0.3.28", "sveltekit-superforms": "^2.20.0", "tailwind-merge": "^2.5.4", diff --git a/frontend/src/lib/components/app-sidebar.svelte b/frontend/src/lib/components/app-sidebar.svelte index ea915d2..5cf3916 100644 --- a/frontend/src/lib/components/app-sidebar.svelte +++ b/frontend/src/lib/components/app-sidebar.svelte @@ -99,7 +99,7 @@ {#if sidebar.open} - diff --git a/frontend/src/lib/components/nav-user.svelte b/frontend/src/lib/components/nav-user.svelte index 591b360..3e52f51 100644 --- a/frontend/src/lib/components/nav-user.svelte +++ b/frontend/src/lib/components/nav-user.svelte @@ -97,7 +97,7 @@ {/if} - + Account diff --git a/frontend/src/lib/components/ui/input/input.svelte b/frontend/src/lib/components/ui/input/input.svelte index 598829f..7616556 100644 --- a/frontend/src/lib/components/ui/input/input.svelte +++ b/frontend/src/lib/components/ui/input/input.svelte @@ -1,22 +1,35 @@ - +{#if type === 'file'} + +{:else} + +{/if} diff --git a/frontend/src/lib/db/schema.ts b/frontend/src/lib/db/schema.ts index 365cd32..23bbff9 100644 --- a/frontend/src/lib/db/schema.ts +++ b/frontend/src/lib/db/schema.ts @@ -6,6 +6,9 @@ import { uuid, boolean, text, + integer, + numeric, + bigint, } from 'drizzle-orm/pg-core' import { relations } from 'drizzle-orm' @@ -40,6 +43,11 @@ export const user = pgTable('user', { .notNull() .default('square'), qrImageBase64: text('qr_image_base64'), + fileStorageUsageInByte: bigint('file_storage_usage_in_byte', { + mode: 'number', + }) + .notNull() + .default(0), }) export const shortener = pgTable('shortener', { @@ -56,6 +64,24 @@ export const shortener = pgTable('shortener', { userId: text('user_id').notNull(), active: boolean('active').notNull().default(true), projectId: text('project_id'), + is_file_upload: boolean('is_file_upload').notNull().default(false), + file_path: text('file_path'), +}) + +export const file = pgTable('file', { + id: text('id').primaryKey(), + userId: text('user_id').notNull(), + projectId: text('project_id'), + key: text('key').notNull(), + name: text('name').notNull(), + size: bigint('size', { + mode: 'number', + }) + .notNull() + .default(0), + eTag: text('etag').notNull(), + createdAt: bigint('created_at_epoch', { mode: 'number' }).notNull(), + updatedAt: bigint('updated_at_epoch', { mode: 'number' }).notNull(), }) export const project = pgTable('project', { @@ -180,3 +206,10 @@ export const sessionRelations = relations(session, ({ one }) => ({ references: [user.id], }), })) + +export const fileRelations = relations(file, ({ one }) => ({ + shortener: one(shortener, { + fields: [file.key], + references: [shortener.file_path], + }), +})) diff --git a/frontend/src/lib/server/types.ts b/frontend/src/lib/server/types.ts index 07ae941..2561bd1 100644 --- a/frontend/src/lib/server/types.ts +++ b/frontend/src/lib/server/types.ts @@ -10,3 +10,19 @@ export const userCreateSchema = z.object({ password: z.string(), password_confirm: z.string(), }) + +export type TigrisNotificationPayload = { + events: { + eventVersion: string + eventSource: string + eventName: 'OBJECT_CREATED_PUT' | 'OBJECT_DELETED' + eventTime: string + bucket: string + object: { + key: string + size: number + eTag: string + } + }[] + sendTime: string +} diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts index 6e17922..0456422 100644 --- a/frontend/src/lib/utils.ts +++ b/frontend/src/lib/utils.ts @@ -91,3 +91,32 @@ Number.prototype.toDecimalPoint = function (decimal: number) { export const isAlphanumeric = (str: string) => { return str.match('^[A-Za-z0-9]+$') } + +// https://stackoverflow.com/questions/10420352/converting-file-size-in-bytes-to-human-readable-string +export function byteToHumanReadable( + bytes: number, + si = true, + dp = 1, +) { + const thresh = si ? 1000 : 1024 + + if (Math.abs(bytes) < thresh) { + return bytes + ' B' + } + + const units = si + ? ['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] + : ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'] + let u = -1 + const r = 10 ** dp + + do { + bytes /= thresh + ++u + } while ( + Math.round(Math.abs(bytes) * r) / r >= thresh && + u < units.length - 1 + ) + + return bytes.toFixed(dp) + ' ' + units[u] +} diff --git a/frontend/src/routes/(app)/dashboard-new/+layout.server.ts b/frontend/src/routes/(app)/dashboard-new/+layout.server.ts deleted file mode 100644 index c274829..0000000 --- a/frontend/src/routes/(app)/dashboard-new/+layout.server.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { LayoutServerLoad } from './$types' - -export const load = (async (event) => { - const user = event.locals.user - return { user } -}) satisfies LayoutServerLoad diff --git a/frontend/src/routes/(app)/dashboard-new/+page.server.ts b/frontend/src/routes/(app)/dashboard-new/+page.server.ts deleted file mode 100644 index c97e0c9..0000000 --- a/frontend/src/routes/(app)/dashboard-new/+page.server.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { redirect } from '@sveltejs/kit' -import type { PageServerLoad, Actions } from './$types' - -export const load = (async (event) => { - redirect(302, '/dashboard-new/project/personal') -}) satisfies PageServerLoad - -export const actions = { - signout: async (event) => { - console.log('signout') - event.cookies.delete('token', { path: '/' }) - redirect(303, '/login') - }, -} satisfies Actions diff --git a/frontend/src/routes/(app)/dashboard-new/account/settings/+page.server.ts b/frontend/src/routes/(app)/dashboard-new/account/settings/+page.server.ts deleted file mode 100644 index 3cddff4..0000000 --- a/frontend/src/routes/(app)/dashboard-new/account/settings/+page.server.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { redirect } from '@sveltejs/kit' -import type { PageServerLoad } from './$types' - -export const load = (async () => { - redirect(300, '/dashboard-new/account/settings/account') -}) satisfies PageServerLoad diff --git a/frontend/src/routes/(app)/dashboard/+layout.server.ts b/frontend/src/routes/(app)/dashboard/+layout.server.ts index e258a58..c274829 100644 --- a/frontend/src/routes/(app)/dashboard/+layout.server.ts +++ b/frontend/src/routes/(app)/dashboard/+layout.server.ts @@ -1,23 +1,6 @@ -import { env } from '$env/dynamic/public' -import { db } from '$lib/db' import type { LayoutServerLoad } from './$types' export const load = (async (event) => { const user = event.locals.user - - const projects = await db.query.project.findMany({ - where: (project, { eq }) => eq(project.userId, user.id), - }) - - const breadcrumbs = [{ name: 'Home', path: '/dashboard' }] - - const page_title = 'Home' - - return { - shortener_url: env.PUBLIC_SHORTENER_URL, - user: user, - breadcrumbs, - page_title, - projects, - } + return { user } }) satisfies LayoutServerLoad diff --git a/frontend/src/routes/(app)/dashboard/+layout.svelte b/frontend/src/routes/(app)/dashboard/+layout.svelte deleted file mode 100644 index dd12220..0000000 --- a/frontend/src/routes/(app)/dashboard/+layout.svelte +++ /dev/null @@ -1,127 +0,0 @@ - - - - - -
- - - - - {#if $page.data.breadcrumbs} - {#each $page.data.breadcrumbs as breadcrumb, index} - {#if index == $page.data.breadcrumbs.length - 1} - - - {breadcrumb.name} - - - {:else} - - - -
- -
-
- {@render children()} -
-
-
-
- - - - -
-
- -
-
-
- Log Out? - - You are about to log out of this account. - -
-
- - -
-
-
-
diff --git a/frontend/src/routes/(app)/dashboard/+page.server.ts b/frontend/src/routes/(app)/dashboard/+page.server.ts index 74f9661..69cd2f4 100644 --- a/frontend/src/routes/(app)/dashboard/+page.server.ts +++ b/frontend/src/routes/(app)/dashboard/+page.server.ts @@ -1,30 +1,8 @@ import { redirect } from '@sveltejs/kit' import type { PageServerLoad, Actions } from './$types' -import { db } from '$lib/db' -import { sql } from 'drizzle-orm' export const load = (async (event) => { - const user = event.locals.user - - const projects = await db.query.project.findMany({ - with: { - shortener: { - with: { - visitor: true, - }, - }, - }, - where: (project, { eq }) => eq(project.userId, user.id), - }) - - const shorteners = await db.query.shortener.findMany({ - with: { - visitor: true, - }, - where: (shortener, { eq }) => eq(shortener.userId, user.id), - }) - - return { projects, shorteners } + redirect(302, '/dashboard/project/personal') }) satisfies PageServerLoad export const actions = { diff --git a/frontend/src/routes/(app)/dashboard/+page.svelte b/frontend/src/routes/(app)/dashboard/+page.svelte deleted file mode 100644 index 332698f..0000000 --- a/frontend/src/routes/(app)/dashboard/+page.svelte +++ /dev/null @@ -1,47 +0,0 @@ - - -
-

Projects

- -
- -
diff --git a/frontend/src/routes/(app)/dashboard-new/account/settings/(components)/sidebar-nav.svelte b/frontend/src/routes/(app)/dashboard/account/settings/(components)/sidebar-nav.svelte similarity index 81% rename from frontend/src/routes/(app)/dashboard-new/account/settings/(components)/sidebar-nav.svelte rename to frontend/src/routes/(app)/dashboard/account/settings/(components)/sidebar-nav.svelte index d2c76a0..3f86799 100644 --- a/frontend/src/routes/(app)/dashboard-new/account/settings/(components)/sidebar-nav.svelte +++ b/frontend/src/routes/(app)/dashboard/account/settings/(components)/sidebar-nav.svelte @@ -6,15 +6,15 @@ const items = [ { title: 'Account', - href: '/dashboard-new/account/settings/account', + href: '/dashboard/account/settings/account', }, { title: 'QR', - href: '/dashboard-new/account/settings/qr', + href: '/dashboard/account/settings/qr', }, { title: 'Security', - href: '/dashboard-new/account/settings/security', + href: '/dashboard/account/settings/security', }, ] as const diff --git a/frontend/src/routes/(app)/dashboard-new/account/settings/+layout.server.ts b/frontend/src/routes/(app)/dashboard/account/settings/+layout.server.ts similarity index 100% rename from frontend/src/routes/(app)/dashboard-new/account/settings/+layout.server.ts rename to frontend/src/routes/(app)/dashboard/account/settings/+layout.server.ts diff --git a/frontend/src/routes/(app)/dashboard-new/account/settings/+layout.svelte b/frontend/src/routes/(app)/dashboard/account/settings/+layout.svelte similarity index 96% rename from frontend/src/routes/(app)/dashboard-new/account/settings/+layout.svelte rename to frontend/src/routes/(app)/dashboard/account/settings/+layout.svelte index b75de50..7f80437 100644 --- a/frontend/src/routes/(app)/dashboard-new/account/settings/+layout.svelte +++ b/frontend/src/routes/(app)/dashboard/account/settings/+layout.svelte @@ -11,7 +11,7 @@
diff --git a/frontend/src/routes/(app)/dashboard/settings/+page.server.ts b/frontend/src/routes/(app)/dashboard/account/settings/+page.server.ts similarity index 73% rename from frontend/src/routes/(app)/dashboard/settings/+page.server.ts rename to frontend/src/routes/(app)/dashboard/account/settings/+page.server.ts index 76567ad..d4f8048 100644 --- a/frontend/src/routes/(app)/dashboard/settings/+page.server.ts +++ b/frontend/src/routes/(app)/dashboard/account/settings/+page.server.ts @@ -2,5 +2,5 @@ import { redirect } from '@sveltejs/kit' import type { PageServerLoad } from './$types' export const load = (async () => { - redirect(300, '/dashboard/settings/account') + redirect(300, '/dashboard/account/settings/account') }) satisfies PageServerLoad diff --git a/frontend/src/routes/(app)/dashboard-new/account/settings/account/+page.server.ts b/frontend/src/routes/(app)/dashboard/account/settings/account/+page.server.ts similarity index 100% rename from frontend/src/routes/(app)/dashboard-new/account/settings/account/+page.server.ts rename to frontend/src/routes/(app)/dashboard/account/settings/account/+page.server.ts diff --git a/frontend/src/routes/(app)/dashboard-new/account/settings/account/+page.svelte b/frontend/src/routes/(app)/dashboard/account/settings/account/+page.svelte similarity index 100% rename from frontend/src/routes/(app)/dashboard-new/account/settings/account/+page.svelte rename to frontend/src/routes/(app)/dashboard/account/settings/account/+page.svelte diff --git a/frontend/src/routes/(app)/dashboard-new/account/settings/account/schema.ts b/frontend/src/routes/(app)/dashboard/account/settings/account/schema.ts similarity index 100% rename from frontend/src/routes/(app)/dashboard-new/account/settings/account/schema.ts rename to frontend/src/routes/(app)/dashboard/account/settings/account/schema.ts diff --git a/frontend/src/routes/(app)/dashboard-new/account/settings/qr/(components)/DemoQR.svelte b/frontend/src/routes/(app)/dashboard/account/settings/qr/(components)/DemoQR.svelte similarity index 100% rename from frontend/src/routes/(app)/dashboard-new/account/settings/qr/(components)/DemoQR.svelte rename to frontend/src/routes/(app)/dashboard/account/settings/qr/(components)/DemoQR.svelte diff --git a/frontend/src/routes/(app)/dashboard-new/account/settings/qr/+page.server.ts b/frontend/src/routes/(app)/dashboard/account/settings/qr/+page.server.ts similarity index 100% rename from frontend/src/routes/(app)/dashboard-new/account/settings/qr/+page.server.ts rename to frontend/src/routes/(app)/dashboard/account/settings/qr/+page.server.ts diff --git a/frontend/src/routes/(app)/dashboard-new/account/settings/qr/+page.svelte b/frontend/src/routes/(app)/dashboard/account/settings/qr/+page.svelte similarity index 100% rename from frontend/src/routes/(app)/dashboard-new/account/settings/qr/+page.svelte rename to frontend/src/routes/(app)/dashboard/account/settings/qr/+page.svelte diff --git a/frontend/src/routes/(app)/dashboard-new/account/settings/qr/schema.ts b/frontend/src/routes/(app)/dashboard/account/settings/qr/schema.ts similarity index 100% rename from frontend/src/routes/(app)/dashboard-new/account/settings/qr/schema.ts rename to frontend/src/routes/(app)/dashboard/account/settings/qr/schema.ts diff --git a/frontend/src/routes/(app)/dashboard-new/account/settings/security/+page.server.ts b/frontend/src/routes/(app)/dashboard/account/settings/security/+page.server.ts similarity index 100% rename from frontend/src/routes/(app)/dashboard-new/account/settings/security/+page.server.ts rename to frontend/src/routes/(app)/dashboard/account/settings/security/+page.server.ts diff --git a/frontend/src/routes/(app)/dashboard-new/account/settings/security/+page.svelte b/frontend/src/routes/(app)/dashboard/account/settings/security/+page.svelte similarity index 100% rename from frontend/src/routes/(app)/dashboard-new/account/settings/security/+page.svelte rename to frontend/src/routes/(app)/dashboard/account/settings/security/+page.svelte diff --git a/frontend/src/routes/(app)/dashboard-new/account/settings/security/schema.ts b/frontend/src/routes/(app)/dashboard/account/settings/security/schema.ts similarity index 100% rename from frontend/src/routes/(app)/dashboard-new/account/settings/security/schema.ts rename to frontend/src/routes/(app)/dashboard/account/settings/security/schema.ts diff --git a/frontend/src/routes/(app)/dashboard/links/(components)/form.svelte b/frontend/src/routes/(app)/dashboard/links/(components)/form.svelte deleted file mode 100644 index d5eac37..0000000 --- a/frontend/src/routes/(app)/dashboard/links/(components)/form.svelte +++ /dev/null @@ -1,269 +0,0 @@ - - - - - - Add Shortner - - - - Add Shortener - - Create A New Shortener Here. Click Add To Save. - - - -
-
Preview
-
-
- {#if isPreviewLoading} -
- -
- {:else if previewData} - -
- {previewData.title} -
- {/if} -
-
-
-
- - - {#snippet children({ props })} - Link - - {/snippet} - - Shortener link - - - - - {#snippet children({ props })} - Project - - - {selectedProjectName} - - - - None - - {#each projects as project} - - {project.name} - - {/each} - - - - {/snippet} - - Shortener Project - - - - - {#snippet children({ props })} - - Custom Code - {/snippet} - - - {#if $formData.custom_code_enable} - - - {#snippet children({ props })} - - {/snippet} - - - Custom Code For The Shortener - - - - {/if} - - - {#snippet children({ props })} - - iOS Link - {/snippet} - - - {#if $formData.ios} - - - {#snippet children({ props })} - - {/snippet} - - - Shortener link for iOS - - - - {/if} - - - {#snippet children({ props })} - - Android Link - {/snippet} - - - {#if $formData.android} - - - {#snippet children({ props })} - - {/snippet} - - - Shortener link for Android - - - - {/if} - - - {#snippet children({ props })} - - Active - {/snippet} - - - - - {#if $submitting} - - {/if} - Add - -
-
-
-
diff --git a/frontend/src/routes/(app)/dashboard/links/+layout.server.ts b/frontend/src/routes/(app)/dashboard/links/+layout.server.ts deleted file mode 100644 index 631f315..0000000 --- a/frontend/src/routes/(app)/dashboard/links/+layout.server.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { LayoutServerLoad } from './$types' - -export const load = (async (event) => { - const { breadcrumbs: parentBreadcrumbs } = await event.parent() - - const breadcrumbs = [ - ...parentBreadcrumbs, - { name: 'Links', path: '/dashboard/links' }, - ] - - const page_title = 'Links' - - return { breadcrumbs, page_title } -}) satisfies LayoutServerLoad diff --git a/frontend/src/routes/(app)/dashboard/links/+page.server.ts b/frontend/src/routes/(app)/dashboard/links/+page.server.ts deleted file mode 100644 index 51d8729..0000000 --- a/frontend/src/routes/(app)/dashboard/links/+page.server.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { db } from '$lib/db' -import type { PageServerLoad } from './$types' -import { shortener } from '$lib/db/schema' -import { fail, setError, superValidate } from 'sveltekit-superforms' -import { zod } from 'sveltekit-superforms/adapters' -import { formSchema } from './schema' -import type { Actions } from './$types' -import { nanoid } from 'nanoid' -import { isAlphanumeric } from '$lib/utils' -import { generateId } from 'lucia' -import type { Project } from '$lib/db/types' - -export const load = (async (event) => { - const user = event.locals.user - - const projects = db.query.project.findMany({ - where: (project, { eq }) => eq(project.userId, user.id), - }) - - return { - projects, - form: await superValidate({ active: true }, zod(formSchema), { - errors: false, - }), - } -}) satisfies PageServerLoad - -export const actions: Actions = { - create: async (event) => { - const form = await superValidate(event, zod(formSchema)) - if (!form.valid) { - return fail(400, { - form, - }) - } - - const user = event.locals.user - let project: Project | undefined = undefined - const selected_project = form.data.project - if (selected_project) { - project = await db.query.project.findFirst({ - where: (project, { eq, and }) => - and( - eq(project.userId, user.id), - eq(project.uuid, selected_project), - ), - }) - } - - if (form.data.custom_code_enable) { - if (!form.data.custom_code) { - return setError( - form, - 'custom_code', - 'Please Enter Custom Code', - ) - } - if (!isAlphanumeric(form.data.custom_code)) { - return setError( - form, - 'custom_code', - 'Code cannot contain special characters', - ) - } - - const customCodeExist = await db.query.shortener.findMany({ - where: (shortener, { eq, and, ne }) => - and(eq(shortener.code, form.data.custom_code)), - with: { - project: true, - }, - }) - - for (const shortener of customCodeExist) { - if (!shortener.project) { - if ( - !project || - (project && !project.enable_custom_domain) - ) { - return setError( - form, - 'custom_code', - 'Duplicated Custom Code', - ) - } - } else { - if (project) { - if ( - !shortener.project.enable_custom_domain && - !project.enable_custom_domain - ) { - return setError( - form, - 'custom_code', - 'Duplicated Custom Code', - ) - } - - if ( - shortener.project.custom_domain === - project.custom_domain - ) { - return setError( - form, - 'custom_code', - 'Duplicated Custom Code', - ) - } - } else { - if (!shortener.project.enable_custom_domain) { - return setError( - form, - 'custom_code', - 'Duplicated Custom Code', - ) - } - } - } - } - } - - const code = form.data.custom_code_enable - ? form.data.custom_code - : nanoid(8) - await db.insert(shortener).values({ - id: generateId(8), - link: form.data.link, - projectId: project?.id, - userId: user.id, - code: code, - ios: form.data.ios, - ios_link: form.data.ios_link, - android: form.data.android, - android_link: form.data.android_link, - }) - - return { form } - }, -} diff --git a/frontend/src/routes/(app)/dashboard/links/+page.svelte b/frontend/src/routes/(app)/dashboard/links/+page.svelte deleted file mode 100644 index c9ddbed..0000000 --- a/frontend/src/routes/(app)/dashboard/links/+page.svelte +++ /dev/null @@ -1,375 +0,0 @@ - - -
- - - - {#await data.projects then projects} -
- {/await} -
- -{#await fetchShorteners(pageNumber, perPage, sortBy, selectedProject, search)} -
-
- {#each [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] as _} - - {/each} -
-
-{:then result} - {#if result.shorteners.length > 0} - -
- {#each result.shorteners as shortener} - - {/each} -
-
-
- { - perPage = parseInt(value) - pageNumber = 1 - }}> - - {perPage} - - - - Page Size - {#each [12, 24, 48, 96] as pageSize} - - {pageSize} - - {/each} - - - - - {#snippet children({ pages, currentPage })} - - - - - - - - {#each pages as page (page.key)} - {#if page.type === 'ellipsis'} - - - - {:else} - - - {page.value} - - - {/if} - {/each} - - - - - - - - {/snippet} - -
- {:else} -
-
-
-
-
-
No Shortener Found
-

Add a new shortener

-
- -
-
-
-
- {/if} -{/await} - - { - if (!open) { - history.back() - } - }}> - - - Shortener QR - - Use this QR code to share the shortener. - - - - - - - - - { - if (!open) { - history.back() - } - }}> - - - Shortener QR - - Use this QR code to share the shortener. - - - - - - - diff --git a/frontend/src/routes/(app)/dashboard/links/[id]/edit/(components)/form.svelte b/frontend/src/routes/(app)/dashboard/links/[id]/edit/(components)/form.svelte deleted file mode 100644 index 628a84b..0000000 --- a/frontend/src/routes/(app)/dashboard/links/[id]/edit/(components)/form.svelte +++ /dev/null @@ -1,204 +0,0 @@ - - -
-
Preview
-
-
- {#if isPreviewLoading} -
- -
- {:else if previewData} - -
- {previewData.title} -
- {/if} -
-
-
- - - - {#snippet children({ props })} - Link - - {/snippet} - - Shortener link - - - - - {#snippet children({ props })} - - Custom Code - {/snippet} - - - {#if $formData.custom_code_enable} - - - {#snippet children({ props })} - - {/snippet} - - - Custom Code For The Shortener - - - - {/if} - - - {#snippet children({ props })} - - iOS Link - {/snippet} - - - {#if $formData.ios} - - - {#snippet children({ props })} - - {/snippet} - - Shortener link for iOS - - - {/if} - - - {#snippet children({ props })} - - Android Link - {/snippet} - - - {#if $formData.android} - - - {#snippet children({ props })} - - {/snippet} - - Shortener link for Android - - - {/if} - - - {#snippet children({ props })} - - Active - {/snippet} - - - - - {#if $submitting} - - {/if} - Save - - diff --git a/frontend/src/routes/(app)/dashboard/links/[id]/edit/+page.server.ts b/frontend/src/routes/(app)/dashboard/links/[id]/edit/+page.server.ts deleted file mode 100644 index dd59952..0000000 --- a/frontend/src/routes/(app)/dashboard/links/[id]/edit/+page.server.ts +++ /dev/null @@ -1,116 +0,0 @@ -import type { PageServerLoad } from './$types' -import { fail, setError, superValidate } from 'sveltekit-superforms' -import { zod } from 'sveltekit-superforms/adapters' -import { formSchema } from './schema' -import type { Actions } from './$types' -import { db } from '$lib/db' -import { redirect } from '@sveltejs/kit' -import { shortener } from '$lib/db/schema' -import { eq } from 'drizzle-orm' -import { isAlphanumeric } from '$lib/utils' - -export const load = (async (event) => { - const user = event.locals.user - const { id } = event.params - - const shortener = await db.query.shortener.findFirst({ - columns: { - id: true, - code: true, - projectId: true, - ios: true, - ios_link: true, - android: true, - android_link: true, - link: true, - active: true, - }, - where: (shortener, { eq, and }) => - and(eq(shortener.code, id), eq(shortener.userId, user.id)), - }) - - if (!shortener) { - redirect(300, `/dashboard/links`) - } - - return { - shortener, - form: await superValidate( - { - ...shortener, - custom_code_enable: true, - custom_code: shortener.code, - }, - zod(formSchema), - { errors: false }, - ), - } -}) satisfies PageServerLoad - -export const actions: Actions = { - default: async (event) => { - const form = await superValidate(event, zod(formSchema)) - if (!form.valid) { - return fail(400, { - form, - }) - } - - const user = event.locals.user - - if (form.data.custom_code_enable) { - if (!form.data.custom_code) { - return setError( - form, - 'custom_code', - 'Please Enter Custom Code', - ) - } - if (!isAlphanumeric(form.data.custom_code)) { - return setError( - form, - 'custom_code', - 'Code cannot contain special characters', - ) - } - - const customCodeExist = await db.query.shortener.findMany({ - where: (shortener, { eq, and, ne }) => - and( - eq(shortener.code, form.data.custom_code), - ne(shortener.id, event.params.id), - ), - with: { - project: true, - }, - }) - - for (const shortener of customCodeExist) { - if (!shortener.project) { - return setError( - form, - 'custom_code', - 'Duplicated Custom Code', - ) - } - } - } - - await db - .update(shortener) - .set({ - link: form.data.link, - userId: user.id, - code: form.data.custom_code_enable - ? form.data.custom_code - : undefined, - ios: form.data.ios, - ios_link: form.data.ios_link, - android: form.data.android, - android_link: form.data.android_link, - }) - .where(eq(shortener.id, event.params.id)) - - return { form } - }, -} diff --git a/frontend/src/routes/(app)/dashboard/links/[id]/edit/+page.svelte b/frontend/src/routes/(app)/dashboard/links/[id]/edit/+page.svelte deleted file mode 100644 index cc5fb37..0000000 --- a/frontend/src/routes/(app)/dashboard/links/[id]/edit/+page.svelte +++ /dev/null @@ -1,20 +0,0 @@ - - -{#if !shallowRouting} - -
-
-
-
-{:else} - -{/if} diff --git a/frontend/src/routes/(app)/dashboard/links/[id]/edit/schema.ts b/frontend/src/routes/(app)/dashboard/links/[id]/edit/schema.ts deleted file mode 100644 index 3f8eb36..0000000 --- a/frontend/src/routes/(app)/dashboard/links/[id]/edit/schema.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { z } from 'zod' - -export const formSchema = z.object({ - link: z.string().url(), - active: z.boolean(), - ios: z.boolean(), - ios_link: z - .union([z.literal(''), z.string().url()]) - .optional() - .nullable(), - android: z.boolean(), - android_link: z - .union([z.literal(''), z.string().url()]) - .optional() - .nullable(), - custom_code_enable: z.boolean(), - custom_code: z.string(), -}) - -export type FormSchema = typeof formSchema diff --git a/frontend/src/routes/(app)/dashboard/links/[id]/qr/(components)/qr.svelte b/frontend/src/routes/(app)/dashboard/links/[id]/qr/(components)/qr.svelte deleted file mode 100644 index 7aaceed..0000000 --- a/frontend/src/routes/(app)/dashboard/links/[id]/qr/(components)/qr.svelte +++ /dev/null @@ -1,116 +0,0 @@ - - -
- - {value} - - {value} -
- - - - QR Link - - - - - Standard - - - With Style - - - - -
-
diff --git a/frontend/src/routes/(app)/dashboard/links/[id]/qr/+page.server.ts b/frontend/src/routes/(app)/dashboard/links/[id]/qr/+page.server.ts deleted file mode 100644 index 898c3e8..0000000 --- a/frontend/src/routes/(app)/dashboard/links/[id]/qr/+page.server.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { db } from '$lib/db' -import { redirect } from '@sveltejs/kit' -import type { PageServerLoad } from './$types' - -export const load = (async (event) => { - const user = event.locals.user - const { id } = event.params - - const shortener = await db.query.shortener.findFirst({ - columns: { - code: true, - }, - where: (shortener, { eq, and, isNull }) => - and(eq(shortener.code, id), isNull(shortener.projectId)), - }) - - if (!shortener) { - redirect(300, `/dashboard/links`) - } - - return { shortener } -}) satisfies PageServerLoad diff --git a/frontend/src/routes/(app)/dashboard/links/[id]/qr/+page.svelte b/frontend/src/routes/(app)/dashboard/links/[id]/qr/+page.svelte deleted file mode 100644 index 49ba36c..0000000 --- a/frontend/src/routes/(app)/dashboard/links/[id]/qr/+page.svelte +++ /dev/null @@ -1,40 +0,0 @@ - - -{#if !shallowRouting} - -
- -
-
-{:else} - -{/if} diff --git a/frontend/src/routes/(app)/dashboard/links/schema.ts b/frontend/src/routes/(app)/dashboard/links/schema.ts deleted file mode 100644 index f7890dc..0000000 --- a/frontend/src/routes/(app)/dashboard/links/schema.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { z } from 'zod' - -export const formSchema = z.object({ - link: z.string().url(), - project: z.string().optional(), - active: z.boolean(), - ios: z.boolean(), - ios_link: z.string().url().optional(), - android: z.boolean(), - android_link: z.string().url().optional(), - custom_code_enable: z.boolean(), - custom_code: z.string(), -}) - -export type FormSchema = typeof formSchema diff --git a/frontend/src/routes/(app)/dashboard-new/project/+page.server.ts b/frontend/src/routes/(app)/dashboard/project/+page.server.ts similarity index 94% rename from frontend/src/routes/(app)/dashboard-new/project/+page.server.ts rename to frontend/src/routes/(app)/dashboard/project/+page.server.ts index 4e1faa2..86eaff4 100644 --- a/frontend/src/routes/(app)/dashboard-new/project/+page.server.ts +++ b/frontend/src/routes/(app)/dashboard/project/+page.server.ts @@ -8,7 +8,7 @@ import { zod } from 'sveltekit-superforms/adapters' import { formSchema } from './[project_id]/(components)/schema' export const load = (async (event) => { - redirect(302, '/dashboard-new/project/personal') + redirect(302, '/dashboard/project/personal') }) satisfies PageServerLoad export const actions: Actions = { diff --git a/frontend/src/routes/(app)/dashboard-new/project/[project_id]/(components)/app-sidebar.svelte b/frontend/src/routes/(app)/dashboard/project/[project_id]/(components)/app-sidebar.svelte similarity index 53% rename from frontend/src/routes/(app)/dashboard-new/project/[project_id]/(components)/app-sidebar.svelte rename to frontend/src/routes/(app)/dashboard/project/[project_id]/(components)/app-sidebar.svelte index e6e98b1..70fdb80 100644 --- a/frontend/src/routes/(app)/dashboard-new/project/[project_id]/(components)/app-sidebar.svelte +++ b/frontend/src/routes/(app)/dashboard/project/[project_id]/(components)/app-sidebar.svelte @@ -7,7 +7,14 @@ import ProjectSwitcher from './project-switcher.svelte' import * as Sidebar from '$lib/components/ui/sidebar/index.js' import type { ComponentProps } from 'svelte' - import { HomeIcon, LinkIcon, SettingsIcon } from 'lucide-svelte' + import { + CloudDownloadIcon, + CloudIcon, + FileIcon, + HomeIcon, + LinkIcon, + SettingsIcon, + } from 'lucide-svelte' import { page } from '$app/stores' import type { User } from 'lucia' import type { Project } from '$lib/db/types' @@ -16,6 +23,9 @@ type SuperValidated, type Infer, } from 'sveltekit-superforms' + import { Progress } from '$lib/components/ui/progress' + import { byteToHumanReadable, cn } from '$lib/utils' + import { Button } from '$lib/components/ui/button' let { ref = $bindable(null), @@ -32,6 +42,8 @@ createProjectForm: SuperValidated> } = $props() + const sidebar = Sidebar.useSidebar() + let projectId = $derived(activeProject?.id || 'personal') let data = $derived({ @@ -49,30 +61,41 @@ navMain: [ { title: 'Overview', - url: `/dashboard-new/project/${projectId}`, + url: `/dashboard/project/${projectId}`, icon: HomeIcon, isActive: - $page.url.pathname === - `/dashboard-new/project/${projectId}`, + $page.url.pathname === `/dashboard/project/${projectId}`, + isUnlocked: true, }, { title: 'Links', - url: `/dashboard-new/project/${projectId}/links`, + url: `/dashboard/project/${projectId}/links`, icon: LinkIcon, isActive: $page.url.pathname.startsWith( - `/dashboard-new/project/${projectId}/links`, + `/dashboard/project/${projectId}/links`, + ), + isUnlocked: true, + }, + { + title: 'Files', + url: `/dashboard/project/${projectId}/file_uploads`, + icon: FileIcon, + isActive: $page.url.pathname.startsWith( + `/dashboard/project/${projectId}/file_uploads`, ), + isUnlocked: user.plan !== 'free', }, { title: 'Settings', url: projectId === 'personal' - ? `/dashboard-new/account/settings` - : `/dashboard-new/project/${projectId}/settings`, + ? `/dashboard/account/settings` + : `/dashboard/project/${projectId}/settings`, icon: SettingsIcon, isActive: $page.url.pathname.startsWith( - `/dashboard-new/project/${projectId}/settings`, + `/dashboard/project/${projectId}/settings`, ), + isUnlocked: true, }, ], }) @@ -95,12 +118,27 @@ {#snippet child({ props })} - - {#if item.icon} - - {/if} - {item.title} - + {#if item.isUnlocked} + + {#if item.icon} + + {/if} + {item.title} + + {:else} + + {#if item.icon} + + {/if} + {item.title} + (Pro) + + {/if} {/snippet} @@ -109,6 +147,23 @@ + {#if sidebar.open && user.plan !== 'free'} +
+
+ + Storage Usage +
+
+ + {byteToHumanReadable(user.fileStorageUsageInByte)} + / 100 GB + + +
+
+ {/if}
diff --git a/frontend/src/routes/(app)/dashboard-new/project/[project_id]/(components)/project-switcher.svelte b/frontend/src/routes/(app)/dashboard/project/[project_id]/(components)/project-switcher.svelte similarity index 96% rename from frontend/src/routes/(app)/dashboard-new/project/[project_id]/(components)/project-switcher.svelte rename to frontend/src/routes/(app)/dashboard/project/[project_id]/(components)/project-switcher.svelte index 78aa330..5e5ee15 100644 --- a/frontend/src/routes/(app)/dashboard-new/project/[project_id]/(components)/project-switcher.svelte +++ b/frontend/src/routes/(app)/dashboard/project/[project_id]/(components)/project-switcher.svelte @@ -80,7 +80,7 @@ Default - +
@@ -97,7 +97,7 @@ Projects {#each projects as project, index (project.name)} - +
@@ -135,7 +135,7 @@ Create A New Project Here. Click Add To Create. - + {#snippet children({ props })} diff --git a/frontend/src/routes/(app)/dashboard-new/project/[project_id]/(components)/schema.ts b/frontend/src/routes/(app)/dashboard/project/[project_id]/(components)/schema.ts similarity index 100% rename from frontend/src/routes/(app)/dashboard-new/project/[project_id]/(components)/schema.ts rename to frontend/src/routes/(app)/dashboard/project/[project_id]/(components)/schema.ts diff --git a/frontend/src/routes/(app)/dashboard-new/project/[project_id]/+layout.server.ts b/frontend/src/routes/(app)/dashboard/project/[project_id]/+layout.server.ts similarity index 93% rename from frontend/src/routes/(app)/dashboard-new/project/[project_id]/+layout.server.ts rename to frontend/src/routes/(app)/dashboard/project/[project_id]/+layout.server.ts index bd59672..48ac8a4 100644 --- a/frontend/src/routes/(app)/dashboard-new/project/[project_id]/+layout.server.ts +++ b/frontend/src/routes/(app)/dashboard/project/[project_id]/+layout.server.ts @@ -29,8 +29,8 @@ export const load = (async (event) => { { name: activeProject?.name || 'Personal', path: activeProject - ? `/dashboard-new/project/${activeProject.id}` - : '/dashboard-new/project/personal', + ? `/dashboard/project/${activeProject.id}` + : '/dashboard/project/personal', }, ] diff --git a/frontend/src/routes/(app)/dashboard-new/project/[project_id]/+layout.svelte b/frontend/src/routes/(app)/dashboard/project/[project_id]/+layout.svelte similarity index 100% rename from frontend/src/routes/(app)/dashboard-new/project/[project_id]/+layout.svelte rename to frontend/src/routes/(app)/dashboard/project/[project_id]/+layout.svelte diff --git a/frontend/src/routes/(app)/dashboard-new/project/[project_id]/+page.server.ts b/frontend/src/routes/(app)/dashboard/project/[project_id]/+page.server.ts similarity index 100% rename from frontend/src/routes/(app)/dashboard-new/project/[project_id]/+page.server.ts rename to frontend/src/routes/(app)/dashboard/project/[project_id]/+page.server.ts diff --git a/frontend/src/routes/(app)/dashboard-new/project/[project_id]/+page.svelte b/frontend/src/routes/(app)/dashboard/project/[project_id]/+page.svelte similarity index 90% rename from frontend/src/routes/(app)/dashboard-new/project/[project_id]/+page.svelte rename to frontend/src/routes/(app)/dashboard/project/[project_id]/+page.svelte index a2c14f4..6148678 100644 --- a/frontend/src/routes/(app)/dashboard-new/project/[project_id]/+page.svelte +++ b/frontend/src/routes/(app)/dashboard/project/[project_id]/+page.svelte @@ -13,7 +13,7 @@
diff --git a/frontend/src/routes/(app)/dashboard-new/project/[project_id]/[...catchall]/+page.svelte b/frontend/src/routes/(app)/dashboard/project/[project_id]/[...catchall]/+page.svelte similarity index 88% rename from frontend/src/routes/(app)/dashboard-new/project/[project_id]/[...catchall]/+page.svelte rename to frontend/src/routes/(app)/dashboard/project/[project_id]/[...catchall]/+page.svelte index 0d2f635..29f957a 100644 --- a/frontend/src/routes/(app)/dashboard-new/project/[project_id]/[...catchall]/+page.svelte +++ b/frontend/src/routes/(app)/dashboard/project/[project_id]/[...catchall]/+page.svelte @@ -11,7 +11,7 @@
Page Not Found
diff --git a/frontend/src/routes/(app)/dashboard/project/[project_id]/file_uploads/(components)/upload-file-card.svelte b/frontend/src/routes/(app)/dashboard/project/[project_id]/file_uploads/(components)/upload-file-card.svelte new file mode 100644 index 0000000..2354462 --- /dev/null +++ b/frontend/src/routes/(app)/dashboard/project/[project_id]/file_uploads/(components)/upload-file-card.svelte @@ -0,0 +1,127 @@ + + + + +
+
+ + {file.name} + + + {byteToHumanReadable(file.size)} + + {#if isCancelled} + (Cancelled) + {:else} + + ({((uploadProgress / uploadMax) * 100).toFixed(1)} %) + + {/if} +
+ +
+ {#if !isCancelled || isCompleted} + + {:else} + + {/if} +
+
diff --git a/frontend/src/routes/(app)/dashboard/project/[project_id]/file_uploads/+page.server.ts b/frontend/src/routes/(app)/dashboard/project/[project_id]/file_uploads/+page.server.ts new file mode 100644 index 0000000..75ad33b --- /dev/null +++ b/frontend/src/routes/(app)/dashboard/project/[project_id]/file_uploads/+page.server.ts @@ -0,0 +1,35 @@ +import type { PageServerLoad } from './$types' +import { db } from '$lib/db' + +export const load = (async (event) => { + const user = event.locals.user + + const { activeProjectId, breadcrumbs: parentBreadcrumbs } = + await event.parent() + + const breadcrumbs = [ + ...parentBreadcrumbs, + { + name: 'Files', + path: `/dashboard/project/${activeProjectId}/file_uploads`, + }, + ] + + const files = db.query.file.findMany({ + where: (file, { and, eq, isNull }) => + and( + eq(file.userId, user.id), + activeProjectId !== 'personal' + ? eq(file.projectId, activeProjectId) + : isNull(file.projectId), + ), + with: { + shortener: true, + }, + }) + + return { + files, + breadcrumbs, + } +}) satisfies PageServerLoad diff --git a/frontend/src/routes/(app)/dashboard/project/[project_id]/file_uploads/+page.svelte b/frontend/src/routes/(app)/dashboard/project/[project_id]/file_uploads/+page.svelte new file mode 100644 index 0000000..e0d1dc3 --- /dev/null +++ b/frontend/src/routes/(app)/dashboard/project/[project_id]/file_uploads/+page.svelte @@ -0,0 +1,323 @@ + + + +
+ + + + + +
+
+ +
+
+
+ + Delete File {deleteKey}? + + + Files And Their Shortener Will Be Permanently Deleted + +
+
+ + +
+
+
+
+ + { + if (!open) { + history.back() + } + }}> + + + Shortener QR + + Use this QR code to share the shortener. + + + + + + + diff --git a/frontend/src/routes/(app)/dashboard-new/project/[project_id]/links/(components)/DeleteShortenerDialog.svelte b/frontend/src/routes/(app)/dashboard/project/[project_id]/links/(components)/DeleteShortenerDialog.svelte similarity index 100% rename from frontend/src/routes/(app)/dashboard-new/project/[project_id]/links/(components)/DeleteShortenerDialog.svelte rename to frontend/src/routes/(app)/dashboard/project/[project_id]/links/(components)/DeleteShortenerDialog.svelte diff --git a/frontend/src/routes/(app)/dashboard-new/project/[project_id]/links/(components)/ShortenerCard.svelte b/frontend/src/routes/(app)/dashboard/project/[project_id]/links/(components)/ShortenerCard.svelte similarity index 88% rename from frontend/src/routes/(app)/dashboard-new/project/[project_id]/links/(components)/ShortenerCard.svelte rename to frontend/src/routes/(app)/dashboard/project/[project_id]/links/(components)/ShortenerCard.svelte index 39db77e..3bbcf59 100644 --- a/frontend/src/routes/(app)/dashboard-new/project/[project_id]/links/(components)/ShortenerCard.svelte +++ b/frontend/src/routes/(app)/dashboard/project/[project_id]/links/(components)/ShortenerCard.svelte @@ -18,6 +18,8 @@ QrCode, TrashIcon, Loader2Icon, + FileIcon, + Split, } from 'lucide-svelte' import DeleteShortenerDialog from './DeleteShortenerDialog.svelte' import ProjectEditLinkPage from '../[linkid]/edit/+page.svelte' @@ -47,7 +49,7 @@ } const getUrl = () => { - return `/dashboard-new/project/${activeProjectId}` + return `/dashboard/project/${activeProjectId}` } let editProjectLinkOpen = $state(false) @@ -57,7 +59,7 @@ const showEditModal = async () => { isLoadingEditProjectData = true editProjectLinkOpen = true - const href = `/dashboard-new/project/${activeProjectId}/links/${shortener.id}/edit` + const href = `/dashboard/project/${activeProjectId}/links/${shortener.id}/edit` const result = await preloadData(href) if (result.type === 'loaded' && result.status === 200) { @@ -94,16 +96,20 @@ - - - - favicon - - + {#if shortener.is_file_upload} + + {:else} + + + + favicon + + + {/if}
@@ -111,11 +117,17 @@
- {shortener.link} + {shortener.is_file_upload + ? shortener.file_path?.split('/').pop() + : shortener.link}
-

{shortener.link}

+

+ {shortener.is_file_upload + ? shortener.file_path?.split('/').pop() + : shortener.link} +

@@ -163,7 +175,7 @@
-
- - - - {/each} -
-{:else} -
-
-
-
-
-
No Project Found
-

Add a new project

-
- -
-
-
-
-{/if} diff --git a/frontend/src/routes/(app)/dashboard/projects/[id]/(components)/form.svelte b/frontend/src/routes/(app)/dashboard/projects/[id]/(components)/form.svelte deleted file mode 100644 index 3c23497..0000000 --- a/frontend/src/routes/(app)/dashboard/projects/[id]/(components)/form.svelte +++ /dev/null @@ -1,232 +0,0 @@ - - - - - - Add Shortner - - - - Add Shortener - - Create A New Shortener Here. Click Add To Save. - - - -
-
Preview
-
-
- {#if isPreviewLoading} -
- -
- {:else if previewData} - -
- {previewData.title} -
- {/if} -
-
-
-
- - - {#snippet children({ props })} - Link - - {/snippet} - - Shortener link - - - - - {#snippet children({ props })} - - Custom Code - {/snippet} - - - {#if $formData.custom_code_enable} - - - {#snippet children({ props })} - - {/snippet} - - - Custom Code For The Shortener - - - - {/if} - - - {#snippet children({ props })} - - iOS Link - {/snippet} - - - {#if $formData.ios} - - - {#snippet children({ props })} - - {/snippet} - - - Shortener link for iOS - - - - {/if} - - - {#snippet children({ props })} - - Android Link - {/snippet} - - - {#if $formData.android} - - - {#snippet children({ props })} - - {/snippet} - - - Shortener link for Android - - - - {/if} - - - {#snippet children({ props })} - - Active - {/snippet} - - - - - {#if $submitting} - - {/if} - Add - -
-
-
-
diff --git a/frontend/src/routes/(app)/dashboard/projects/[id]/+layout.server.ts b/frontend/src/routes/(app)/dashboard/projects/[id]/+layout.server.ts deleted file mode 100644 index e57912f..0000000 --- a/frontend/src/routes/(app)/dashboard/projects/[id]/+layout.server.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { db } from '$lib/db' -import { redirect } from '@sveltejs/kit' -import type { LayoutServerLoad } from './$types' - -export const load = (async (event) => { - const { id } = event.params - try { - const user = event.locals.user - const project = await db.query.project.findFirst({ - where: (project, { eq, and }) => - and(eq(project.userId, user.id), eq(project.uuid, id)), - }) - - if (!project) { - redirect(300, '/dashboard/projects') - } - - const { breadcrumbs: parentBreadcrumbs } = await event.parent() - const breadcrumbs = [ - ...parentBreadcrumbs, - { - name: project.name, - path: `/dashboard/projects/${project.uuid}`, - }, - ] - return { breadcrumbs, project } - } catch (e) { - redirect(300, '/dashboard/projects') - } -}) satisfies LayoutServerLoad diff --git a/frontend/src/routes/(app)/dashboard/projects/[id]/+layout.svelte b/frontend/src/routes/(app)/dashboard/projects/[id]/+layout.svelte deleted file mode 100644 index a6bae07..0000000 --- a/frontend/src/routes/(app)/dashboard/projects/[id]/+layout.svelte +++ /dev/null @@ -1,43 +0,0 @@ - - -
-
-

- {data.project.name} -

- - {#key $page.url.pathname} - -
- - -
-
- {/key} -
-
- -
- {@render children()} -
diff --git a/frontend/src/routes/(app)/dashboard/projects/[id]/+page.server.ts b/frontend/src/routes/(app)/dashboard/projects/[id]/+page.server.ts deleted file mode 100644 index dd1cd9e..0000000 --- a/frontend/src/routes/(app)/dashboard/projects/[id]/+page.server.ts +++ /dev/null @@ -1,193 +0,0 @@ -import { db } from '$lib/db' -import { - and, - asc, - desc, - eq, - getTableColumns, - ilike, - sql, -} from 'drizzle-orm' -import type { PageServerLoad } from './$types' -import { shortener, visitor } from '$lib/db/schema' -import { fail, setError, superValidate } from 'sveltekit-superforms' -import { zod } from 'sveltekit-superforms/adapters' -import { formSchema } from './schema' -import type { Actions } from './$types' -import { nanoid } from 'nanoid' -import { isAlphanumeric } from '$lib/utils' -import { generateId } from 'lucia' - -export const load = (async (event) => { - const { project: selectedProject } = await event.parent() - - const user = event.locals.user - - const search = event.url.searchParams.get('search') - let sortBy = event.url.searchParams.get('sortBy') - let page = parseInt(event.url.searchParams.get('page') ?? '1') - let perPage = parseInt( - event.url.searchParams.get('perPage') ?? '12', - ) - - if (isNaN(page)) { - page = 1 - } - - if (isNaN(perPage)) { - perPage = 10 - } - - if ( - sortBy !== 'latest' && - sortBy !== 'oldest' && - sortBy !== 'most_visited' - ) { - sortBy = 'latest' - } - - const shortenerColumns = getTableColumns(shortener) - const shorteners = db - .select({ - ...shortenerColumns, - visitorCount: sql`count(${visitor.id})`, - }) - .from(shortener) - .where( - and( - eq(shortener.userId, user.id), - eq(shortener.projectId, selectedProject.id), - search - ? ilike(shortener.link, `%${decodeURI(search)}%`) - : undefined, - ), - ) - .leftJoin(visitor, eq(shortener.id, visitor.shortenerId)) - .groupBy(shortener.id) - .offset(perPage * (page - 1)) - .limit(perPage) - - if (sortBy === 'latest') { - shorteners.orderBy(desc(shortener.createdAt)) - } else if (sortBy === 'oldest') { - shorteners.orderBy(asc(shortener.createdAt)) - } else if (sortBy === 'most_visited') { - shorteners.orderBy(sql`count(${visitor.id}) desc`) - } - - const pagination = db - .select({ - total: sql`count(*)`.as('total'), - }) - .from(shortener) - .where( - and( - eq(shortener.userId, user.id), - eq(shortener.projectId, selectedProject.id), - search - ? ilike(shortener.link, `%${decodeURI(search)}%`) - : undefined, - ), - ) - - return { - selectedProject, - shorteners, - page, - perPage, - search, - sortBy, - pagination, - form: await superValidate({ active: true }, zod(formSchema), { - errors: false, - }), - } -}) satisfies PageServerLoad - -export const actions: Actions = { - create: async (event) => { - const form = await superValidate(event, zod(formSchema)) - if (!form.valid) { - return fail(400, { - form, - }) - } - - const { id } = event.params - const user = event.locals.user - const project = await db.query.project.findFirst({ - where: (project, { eq, and }) => - and(eq(project.userId, user.id), eq(project.uuid, id)), - }) - - if (!project) { - return fail(400, { - form, - }) - } - - if (form.data.custom_code_enable) { - if (!form.data.custom_code) { - return setError( - form, - 'custom_code', - 'Please Enter Custom Code', - ) - } - if (!isAlphanumeric(form.data.custom_code)) { - return setError( - form, - 'custom_code', - 'Code cannot contain special characters', - ) - } - - const customCodeExist = await db.query.shortener.findMany({ - where: (shortener, { eq, and, ne }) => - and(eq(shortener.code, form.data.custom_code)), - with: { - project: true, - }, - }) - - for (const shortener of customCodeExist) { - if (!shortener.project && !project.enable_custom_domain) { - return setError( - form, - 'custom_code', - 'Duplicated Custom Code', - ) - } - - if (!shortener.project) continue - - if ( - shortener.project.custom_domain === project.custom_domain - ) { - return setError( - form, - 'custom_code', - 'Duplicated Custom Code', - ) - } - } - } - - const code = form.data.custom_code_enable - ? form.data.custom_code - : nanoid(8) - await db.insert(shortener).values({ - id: generateId(8), - link: form.data.link, - projectId: project.id, - userId: user.id, - code: code, - ios: form.data.ios, - ios_link: form.data.ios_link, - android: form.data.android, - android_link: form.data.android_link, - }) - - return { form } - }, -} diff --git a/frontend/src/routes/(app)/dashboard/projects/[id]/+page.svelte b/frontend/src/routes/(app)/dashboard/projects/[id]/+page.svelte deleted file mode 100644 index 0ec2332..0000000 --- a/frontend/src/routes/(app)/dashboard/projects/[id]/+page.svelte +++ /dev/null @@ -1,266 +0,0 @@ - - -
- - -
-
- -{#await fetchShorteners(pageNumber, perPage, sortBy, selectedProject, search)} -
- {#each [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] as _} - - {/each} -
-{:then result} - {#if result.shorteners.length > 0} - -
- {#each result.shorteners as shortener} - - {/each} -
-
-
- { - perPage = parseInt(value) - pageNumber = 1 - }}> - - {perPage} - - - - Page Size - {#each [12, 24, 48, 96] as pageSize} - - {pageSize} - - {/each} - - - - - {#snippet children({ pages, currentPage })} - - - - - - - - {#each pages as page (page.key)} - {#if page.type === 'ellipsis'} - - - - {:else} - - - {page.value} - - - {/if} - {/each} - - - - - - - - {/snippet} - -
- {:else} -
-
-
-
-
-
No Shortener Found
-

Add a new shortener

-
- -
-
-
-
- {/if} -{/await} - - { - if (!open) { - history.back() - } - }}> - - - Edit Shortener - - Edit Shortener Here. Click Save To Save. - - - - - - - - - { - if (!open) { - history.back() - } - }}> - - - Shortener QR - - Use this QR code to share the shortener. - - - - - - - diff --git a/frontend/src/routes/(app)/dashboard/projects/[id]/[...catchall]/+page.svelte b/frontend/src/routes/(app)/dashboard/projects/[id]/[...catchall]/+page.svelte deleted file mode 100644 index 101da74..0000000 --- a/frontend/src/routes/(app)/dashboard/projects/[id]/[...catchall]/+page.svelte +++ /dev/null @@ -1,8 +0,0 @@ -
-
-
-
404
-
Page Not Found
-
-
-
diff --git a/frontend/src/routes/(app)/dashboard/projects/[id]/links/[linkid]/edit/(components)/form.svelte b/frontend/src/routes/(app)/dashboard/projects/[id]/links/[linkid]/edit/(components)/form.svelte deleted file mode 100644 index 6aaa314..0000000 --- a/frontend/src/routes/(app)/dashboard/projects/[id]/links/[linkid]/edit/(components)/form.svelte +++ /dev/null @@ -1,208 +0,0 @@ - - -
-
Preview
-
-
- {#if isPreviewLoading} -
- -
- {:else if previewData} - -
- {previewData.title} -
- {/if} -
-
-
- - - - {#snippet children({ props })} - Link - - {/snippet} - - Shortener link - - - - - {#snippet children({ props })} - - Custom Code - {/snippet} - - - {#if $formData.custom_code_enable} - - - {#snippet children({ props })} - - {/snippet} - - - Custom Code For The Shortener - - - - {/if} - - - {#snippet children({ props })} - - iOS Link - {/snippet} - - - {#if $formData.ios} - - - {#snippet children({ props })} - - {/snippet} - - Shortener link for iOS - - - {/if} - - - {#snippet children({ props })} - - Android Link - {/snippet} - - - {#if $formData.android} - - - {#snippet children({ props })} - - {/snippet} - - Shortener link for Android - - - {/if} - - - {#snippet children({ props })} - - Active - {/snippet} - - - - - {#if $submitting} - - {/if} - Save - - diff --git a/frontend/src/routes/(app)/dashboard/projects/[id]/links/[linkid]/edit/+page.server.ts b/frontend/src/routes/(app)/dashboard/projects/[id]/links/[linkid]/edit/+page.server.ts deleted file mode 100644 index 8d29e26..0000000 --- a/frontend/src/routes/(app)/dashboard/projects/[id]/links/[linkid]/edit/+page.server.ts +++ /dev/null @@ -1,141 +0,0 @@ -import type { PageServerLoad } from './$types' -import { fail, setError, superValidate } from 'sveltekit-superforms' -import { zod } from 'sveltekit-superforms/adapters' -import { formSchema } from './schema' -import type { Actions } from './$types' -import { db } from '$lib/db' -import { redirect } from '@sveltejs/kit' -import { shortener } from '$lib/db/schema' -import { eq } from 'drizzle-orm' -import { isAlphanumeric } from '$lib/utils' - -export const load = (async (event) => { - const { project: selectedProject } = await event.parent() - const { linkid } = event.params - - const shortener = await db.query.shortener.findFirst({ - columns: { - id: true, - code: true, - ios: true, - ios_link: true, - android: true, - android_link: true, - link: true, - active: true, - }, - where: (shortener, { eq, and }) => - and( - eq(shortener.id, linkid), - eq(shortener.projectId, selectedProject.id), - ), - }) - - if (!shortener) { - redirect(300, `/dashboard/projects/${selectedProject.id}`) - } - - return { - shortener, - form: await superValidate( - { - ...shortener, - custom_code_enable: true, - custom_code: shortener.code, - }, - zod(formSchema), - ), - } -}) satisfies PageServerLoad - -export const actions: Actions = { - default: async (event) => { - const form = await superValidate(event, zod(formSchema)) - if (!form.valid) { - return fail(400, { - form, - }) - } - - const { id } = event.params - const user = event.locals.user - const project = await db.query.project.findFirst({ - where: (project, { eq, and }) => - and(eq(project.userId, user.id), eq(project.uuid, id)), - }) - - if (!project) { - return fail(400, { - form, - }) - } - - if (form.data.custom_code_enable) { - if (!form.data.custom_code) { - return setError( - form, - 'custom_code', - 'Please Enter Custom Code', - ) - } - if (!isAlphanumeric(form.data.custom_code)) { - return setError( - form, - 'custom_code', - 'Code cannot contain special characters', - ) - } - - const customCodeExist = await db.query.shortener.findMany({ - where: (shortener, { eq, and, ne }) => - and( - eq(shortener.code, form.data.custom_code), - ne(shortener.id, event.params.linkid), - ), - with: { - project: true, - }, - }) - - for (const shortener of customCodeExist) { - if (!shortener.project && !project.enable_custom_domain) { - return setError( - form, - 'custom_code', - 'Duplicated Custom Code', - ) - } - - if (!shortener.project) continue - - if ( - shortener.project.custom_domain === project.custom_domain - ) { - return setError( - form, - 'custom_code', - 'Duplicated Custom Code', - ) - } - } - } - - await db - .update(shortener) - .set({ - link: form.data.link, - projectId: project.id, - userId: user.id, - code: form.data.custom_code_enable - ? form.data.custom_code - : undefined, - ios: form.data.ios, - ios_link: form.data.ios_link, - android: form.data.android, - android_link: form.data.android_link, - }) - .where(eq(shortener.id, event.params.linkid)) - - return { form } - }, -} diff --git a/frontend/src/routes/(app)/dashboard/projects/[id]/links/[linkid]/edit/+page.svelte b/frontend/src/routes/(app)/dashboard/projects/[id]/links/[linkid]/edit/+page.svelte deleted file mode 100644 index dbafcfc..0000000 --- a/frontend/src/routes/(app)/dashboard/projects/[id]/links/[linkid]/edit/+page.svelte +++ /dev/null @@ -1,26 +0,0 @@ - - -{#if !shallowRouting} - -
-
-
-
-{:else} - -{/if} diff --git a/frontend/src/routes/(app)/dashboard/projects/[id]/links/[linkid]/edit/schema.ts b/frontend/src/routes/(app)/dashboard/projects/[id]/links/[linkid]/edit/schema.ts deleted file mode 100644 index 88634ef..0000000 --- a/frontend/src/routes/(app)/dashboard/projects/[id]/links/[linkid]/edit/schema.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { z } from 'zod' - -export const formSchema = z.object({ - link: z.string().url(), - project: z.string().optional(), - active: z.boolean(), - ios: z.boolean(), - ios_link: z - .union([z.literal(''), z.string().url()]) - .optional() - .nullable(), - android: z.boolean(), - android_link: z - .union([z.literal(''), z.string().url()]) - .optional() - .nullable(), - custom_code_enable: z.boolean(), - custom_code: z.string(), -}) - -export type FormSchema = typeof formSchema diff --git a/frontend/src/routes/(app)/dashboard/projects/[id]/links/[linkid]/qr/(components)/qr.svelte b/frontend/src/routes/(app)/dashboard/projects/[id]/links/[linkid]/qr/(components)/qr.svelte deleted file mode 100644 index 431761a..0000000 --- a/frontend/src/routes/(app)/dashboard/projects/[id]/links/[linkid]/qr/(components)/qr.svelte +++ /dev/null @@ -1,116 +0,0 @@ - - -
- - {value} - - {value} -
- - - - QR Link - - - - - Standard - - - With Style - - - - -
-
diff --git a/frontend/src/routes/(app)/dashboard/projects/[id]/links/[linkid]/qr/+page.server.ts b/frontend/src/routes/(app)/dashboard/projects/[id]/links/[linkid]/qr/+page.server.ts deleted file mode 100644 index 350b855..0000000 --- a/frontend/src/routes/(app)/dashboard/projects/[id]/links/[linkid]/qr/+page.server.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { db } from '$lib/db' -import { redirect } from '@sveltejs/kit' -import type { PageServerLoad } from './$types' - -export const load = (async (event) => { - const { project: selectedProject } = await event.parent() - const { linkid } = event.params - - const shortener = await db.query.shortener.findFirst({ - columns: { - code: true, - }, - where: (shortener, { eq, and }) => - and( - eq(shortener.code, linkid), - eq(shortener.projectId, selectedProject.id), - ), - }) - - if (!shortener) { - redirect(300, `/dashboard/projects/${selectedProject.id}`) - } - - return { shortener, project: selectedProject } -}) satisfies PageServerLoad diff --git a/frontend/src/routes/(app)/dashboard/projects/[id]/links/[linkid]/qr/+page.svelte b/frontend/src/routes/(app)/dashboard/projects/[id]/links/[linkid]/qr/+page.svelte deleted file mode 100644 index 10f0d94..0000000 --- a/frontend/src/routes/(app)/dashboard/projects/[id]/links/[linkid]/qr/+page.svelte +++ /dev/null @@ -1,39 +0,0 @@ - - -{#if !shallowRouting} - -
- -
-
-{:else} - -{/if} diff --git a/frontend/src/routes/(app)/dashboard/projects/[id]/schema.ts b/frontend/src/routes/(app)/dashboard/projects/[id]/schema.ts deleted file mode 100644 index 1b6547a..0000000 --- a/frontend/src/routes/(app)/dashboard/projects/[id]/schema.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { z } from 'zod' - -export const formSchema = z.object({ - link: z.string().url(), - active: z.boolean(), - ios: z.boolean(), - ios_link: z.string().url().optional(), - android: z.boolean(), - android_link: z.string().url().optional(), - custom_code_enable: z.boolean(), - custom_code: z.string(), -}) - -export type FormSchema = typeof formSchema diff --git a/frontend/src/routes/(app)/dashboard/projects/[id]/settings/(components)/DemoQR.svelte b/frontend/src/routes/(app)/dashboard/projects/[id]/settings/(components)/DemoQR.svelte deleted file mode 100644 index a92721f..0000000 --- a/frontend/src/routes/(app)/dashboard/projects/[id]/settings/(components)/DemoQR.svelte +++ /dev/null @@ -1,88 +0,0 @@ - - -{value} diff --git a/frontend/src/routes/(app)/dashboard/projects/[id]/settings/(components)/dns-info.svelte b/frontend/src/routes/(app)/dashboard/projects/[id]/settings/(components)/dns-info.svelte deleted file mode 100644 index 7ceff19..0000000 --- a/frontend/src/routes/(app)/dashboard/projects/[id]/settings/(components)/dns-info.svelte +++ /dev/null @@ -1,85 +0,0 @@ - - - - - {#if cname_record} - CNAME - {/if} - {#if a_record || aaaa_record} - A and AAAA - {/if} - - {#if cname_record} - - - - Type - Host - Value - - - CNAME - {host} - {cname_record} - - - - {/if} - {#if a_record || aaaa_record} - -
- {#if a_record} - - - Type - Host - Value - - - A - {host} - {a_record} - - - {/if} - {#if aaaa_record} - - - Type - Host - Value - - - AAAA - {host} - {aaaa_record} - - - {/if} -
-
- {/if} -
diff --git a/frontend/src/routes/(app)/dashboard/projects/[id]/settings/(components)/dns-tooltip.svelte b/frontend/src/routes/(app)/dashboard/projects/[id]/settings/(components)/dns-tooltip.svelte deleted file mode 100644 index b5d669b..0000000 --- a/frontend/src/routes/(app)/dashboard/projects/[id]/settings/(components)/dns-tooltip.svelte +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - - {#if custom_ip} - {custom_ip} - {:else if cname_record} - {cname_record} - {:else if a_record} - {a_record} - {:else if aaaa_record} - {aaaa_record} - {:else} - {'Public IP not found'} - {/if} - - - {#if custom_ip} -
- {'Create a CNAME/ALIAS record for ' + - domain + - ' to ' + - custom_ip} -
- {/if} - {#if cname_record} -
- {'Create a CNAME/ALIAS record for ' + - domain + - ' to ' + - cname_record} -
- {/if} - {#if a_record} -
- {'Create a A record for ' + domain + ' to ' + a_record} -
- {/if} - - {#if aaaa_record} -
- {'Create a AAAA record for ' + - domain + - ' to ' + - aaaa_record} -
- {/if} - {#if !(custom_ip || cname_record || a_record || aaaa_record)} -
- {'Public IP not found'} -
- {/if} -
-
-
diff --git a/frontend/src/routes/(app)/dashboard/projects/[id]/settings/(components)/form.svelte b/frontend/src/routes/(app)/dashboard/projects/[id]/settings/(components)/form.svelte deleted file mode 100644 index 0ccad24..0000000 --- a/frontend/src/routes/(app)/dashboard/projects/[id]/settings/(components)/form.svelte +++ /dev/null @@ -1,250 +0,0 @@ - - - - - - {#snippet children({ props })} - Name - - {/snippet} - - Update Project Name - - -
- -
- - - {#snippet children({ props })} - Background Color - - {/snippet} - - QR Code background color - - - - - {#snippet children({ props })} - Foreground Color - - {/snippet} - - QR Code foreground color - - - - - {#snippet children({ props })} - - Image (Pro) - -
- {#if !$formData.qrImage && !qrImageBase64} - - {:else} - - {/if} -
- Click to edit - { - const file = e.currentTarget.files?.item(0) - if (!file) return - - if (file.size > 2097152) { - toast.error('Too Big! Max file size is 2MB') - return - } - $formData.qrImage = file - }} /> - {/snippet} -
- -
- - - {#snippet children({ props })} - - Corner Square Style (Pro) - - - {/snippet} - -
- - - -
-
- - - {#snippet children({ props })} - - Dot Style (Pro) - - - {/snippet} - -
- - -
-
- - {#if $submitting} - - {/if} - Save - - diff --git a/frontend/src/routes/(app)/dashboard/projects/[id]/settings/+page.server.ts b/frontend/src/routes/(app)/dashboard/projects/[id]/settings/+page.server.ts deleted file mode 100644 index 7eca1a3..0000000 --- a/frontend/src/routes/(app)/dashboard/projects/[id]/settings/+page.server.ts +++ /dev/null @@ -1,447 +0,0 @@ -import { db } from '$lib/db' -import type { PageServerLoad, Actions } from './$types' -import { - message, - setError, - setMessage, - superValidate, - fail, - withFiles, -} from 'sveltekit-superforms' -import { - formSchema, - deleteSchema, - customDomainFormSchema, - enableCustomDomainFormSchema, -} from './schema' -import { zod } from 'sveltekit-superforms/adapters' -import { - project as projectTable, - shortener, - visitor, -} from '$lib/db/schema' -import { and, eq } from 'drizzle-orm' -import { - checkDomainAvailable, - createCustomDomain, - deleteCustomDomain, -} from '$lib/server/domain' -import { env } from '$env/dynamic/private' - -export const load = (async (event) => { - const { project } = await event.parent() - - let cnameRecord = '' - let aRecord = '' - let aaaaRecord = '' - - const provider = env.PRIVATE_HOSTING_PROVIDER - - if (provider === 'fly.io') { - cnameRecord = env.PRIVATE_FLYIO_CNAME - aRecord = env.PRIVATE_FLYIO_IPV4 - aaaaRecord = env.PRIVATE_FLYIO_IPV6 - } else if (provider === 'railway') { - } else { - aRecord = env.PUBLIC_SHORTENER_IP || '' - } - - return { - form: await superValidate( - { - name: project.name, - qr_background: project.qr_background, - qr_foreground: project.qr_foreground, - qrCornerSquareStyle: project.qrCornerSquareStyle, - qrDotStyle: project.qrDotStyle, - }, - zod(formSchema), - ), - qrImageBase64: project.qrImageBase64, - enableCustomDomainForm: await superValidate( - { enableDomain: project.custom_domain || '' }, - zod(enableCustomDomainFormSchema), - ), - customDomainForm: await superValidate( - { domain: project.custom_domain || '' }, - zod(customDomainFormSchema), - ), - deleteForm: await superValidate( - { deleteShorteners: true }, - zod(deleteSchema), - { - id: 'deleteProject', - }, - ), - cnameRecord, - aRecord, - aaaaRecord, - } -}) satisfies PageServerLoad - -export const actions: Actions = { - update: async (event) => { - const form = await superValidate(event, zod(formSchema)) - if (!form.valid) { - return fail( - 400, - withFiles({ - form, - }), - ) - } - const user = event.locals.user - - const qrImage = form.data.qrImage - - const qrImageType = qrImage ? qrImage.type : undefined - const qrImageBlob = qrImage - ? await qrImage.arrayBuffer() - : undefined - const qrImageBase64 = qrImageBlob - ? Buffer.from(qrImageBlob).toString('base64') - : undefined - - await db - .update(projectTable) - .set({ - name: form.data.name, - qr_background: form.data.qr_background, - qr_foreground: form.data.qr_foreground, - qrCornerSquareStyle: - user.plan !== 'free' - ? form.data.qrCornerSquareStyle - : undefined, - qrDotStyle: - user.plan !== 'free' ? form.data.qrDotStyle : undefined, - qrImageBase64: - user.plan !== 'free' - ? qrImage - ? `data:${qrImageType};base64,${qrImageBase64}` - : undefined - : undefined, - }) - .where( - and( - eq(projectTable.uuid, event.params.id), - eq(projectTable.userId, user.id), - ), - ) - - return withFiles({ - form, - }) - }, - enable_custom_domain: async (event) => { - const form = await superValidate( - event, - zod(enableCustomDomainFormSchema), - ) - if (!form.valid) { - return fail(400, { - form, - }) - } - - if (form.data.enableDomain === '') { - return setError(form, 'enableDomain', 'domain cannot be empty') - } - - const userId = event.locals.user.id - - if ( - !event.locals.user.email_verified && - env.PRIVATE_MAIL_PROVIDER && - env.PRIVATE_MAIL_PROVIDER !== '' - ) { - return setError( - form, - 'enableDomain', - 'Please verify your email account', - ) - } - - if (event.locals.user.plan === 'free') { - return setError( - form, - 'enableDomain', - 'Please upgrade your account to pro plan to use this feature', - ) - } - - if (event.locals.user.plan === 'pro') { - const projectsWithEnabledDomains = - await db.query.project.findMany({ - where: (projectTable, { eq, and }) => - and( - eq(projectTable.userId, userId), - eq(projectTable.enable_custom_domain, true), - ), - }) - - if (projectsWithEnabledDomains.length >= 5) { - return setError( - form, - 'enableDomain', - 'You are only allowed to use maximum 5 custom domains in pro plan', - ) - } - } - - const existingProject = await db.query.project.findFirst({ - where: (projectTable, { eq, and }) => - and( - eq(projectTable.uuid, event.params.id), - eq(projectTable.userId, userId), - ), - }) - - if (!existingProject) { - return fail(400, { message: 'Project not found' }) - } - - const sameDomainDifferentProject = - await db.query.project.findFirst({ - where: (projectTable, { eq, and, ne }) => - and( - ne(projectTable.uuid, event.params.id), - eq(projectTable.custom_domain, form.data.enableDomain), - ), - }) - - if (sameDomainDifferentProject) { - return setError(form, 'enableDomain', 'Domain already taken') - } - - const domainAvailable = await checkDomainAvailable( - form.data.enableDomain, - ) - - if (!domainAvailable) { - return setError(form, 'enableDomain', 'Domain is not available') - } - - const customDomain = await createCustomDomain( - form.data.enableDomain, - ) - - if (!customDomain.success) { - return setError( - form, - 'enableDomain', - 'Cannot create custom domain', - ) - } - - await db - .update(projectTable) - .set({ - custom_domain: form.data.enableDomain, - custom_domain_id: customDomain.id, - custom_ip: customDomain.ip, - enable_custom_domain: true, - }) - .where( - and( - eq(projectTable.uuid, event.params.id), - eq(projectTable.userId, userId), - ), - ) - - return setMessage(form, 'Custom Domain Enabled') - }, - disable_custom_domain: async (event) => { - return { message: 'Disabling custom domain is unavailable' } - const userId = event.locals.user.id - - const existingProject = await db.query.project.findFirst({ - where: (projectTable, { eq, and }) => - and( - eq(projectTable.uuid, event.params.id), - eq(projectTable.userId, userId), - ), - }) - - if (!existingProject) { - return fail(400, { message: 'Project not found' }) - } - - const deleteOldCustomDomain = await deleteCustomDomain( - existingProject.custom_domain_id, - ) - - if (!deleteOldCustomDomain.success) { - return { message: 'Cannot delete old custom domain' } - } - - await db - .update(projectTable) - .set({ - enable_custom_domain: false, - }) - .where( - and( - eq(projectTable.uuid, event.params.id), - eq(projectTable.userId, userId), - ), - ) - - return { message: 'Custom domain disabled' } - }, - update_custom_domain: async (event) => { - const form = await superValidate( - event, - zod(customDomainFormSchema), - ) - if (!form.valid) { - return fail(400, { - form, - }) - } - - const userId = event.locals.user.id - - const existingProject = await db.query.project.findFirst({ - where: (projectTable, { eq, and }) => - and( - eq(projectTable.uuid, event.params.id), - eq(projectTable.userId, userId), - ), - }) - - if (!existingProject || !existingProject.enable_custom_domain) { - return fail(400, { - form, - }) - } - - const sameDomainDifferentProject = - await db.query.project.findFirst({ - where: (projectTable, { eq, and, ne }) => - and( - ne(projectTable.uuid, event.params.id), - eq(projectTable.custom_domain, form.data.domain), - ), - }) - - if (sameDomainDifferentProject) { - return setError(form, 'domain', 'Domain already taken') - } - - const sameDomainSameProject = await db.query.project.findFirst({ - where: (projectTable, { eq, and }) => - and( - eq(projectTable.uuid, event.params.id), - eq(projectTable.custom_domain, form.data.domain), - ), - }) - - if (sameDomainSameProject) { - return { form } - } - - const domainAvailable = await checkDomainAvailable( - form.data.domain, - ) - - if (!domainAvailable) { - return setError(form, 'domain', 'Domain is not available') - } - - const customDomain = await createCustomDomain(form.data.domain) - - if (!customDomain.success) { - return setError(form, 'domain', 'Cannot create custom domain') - } - - const deleteOldCustomDomain = await deleteCustomDomain( - existingProject.custom_domain_id, - ) - - if (!deleteOldCustomDomain.success) { - return setError( - form, - 'domain', - 'Cannot delete old custom domain', - ) - } - - await db - .update(projectTable) - .set({ - custom_domain: form.data.domain, - custom_domain_id: customDomain.id, - custom_ip: customDomain.ip, - }) - .where( - and( - eq(projectTable.uuid, event.params.id), - eq(projectTable.userId, userId), - ), - ) - - return { - form, - } - }, - delete: async (event) => { - const userId = event.locals.user.id - - const form = await superValidate(event, zod(deleteSchema)) - - if (!form.valid) { - return fail(400, { - form, - }) - } - - try { - const deletedProject = await db - .delete(projectTable) - .where( - and( - eq(projectTable.uuid, event.params.id), - eq(projectTable.userId, userId), - ), - ) - .returning() - - await deleteCustomDomain(deletedProject[0].custom_domain_id) - - if (form.data.deleteShorteners) { - const deletedShorteners = await db - .delete(shortener) - .where( - and( - eq(shortener.projectId, deletedProject[0].id), - eq(shortener.userId, userId), - ), - ) - .returning() - deletedShorteners.map(async (shortener) => { - await db - .delete(visitor) - .where(eq(visitor.shortenerId, shortener.id)) - }) - } else { - await db - .update(shortener) - .set({ projectId: null }) - .where( - and( - eq(shortener.projectId, deletedProject[0].id), - eq(shortener.userId, userId), - ), - ) - } - - return { - form, - } - } catch (error) { - return fail(400, { - form, - }) - } - }, -} diff --git a/frontend/src/routes/(app)/dashboard/projects/[id]/settings/+page.svelte b/frontend/src/routes/(app)/dashboard/projects/[id]/settings/+page.svelte deleted file mode 100644 index 503af33..0000000 --- a/frontend/src/routes/(app)/dashboard/projects/[id]/settings/+page.svelte +++ /dev/null @@ -1,397 +0,0 @@ - - - -
-
-

Custom Domain

-

- Update project domain. -

-
- - - - -
-
- {#if data.project.domain_status === 'pending'} - - {:else if data.project.domain_status === 'verified'} - - {:else if data.project.domain_status === 'disabled'} - - {/if} -
-
- {#if data.project.enable_custom_domain && data.project.custom_domain} - - {data.project.custom_domain} - - - custom domain - - - - - {:else} - - {env.PUBLIC_SHORTENER_URL} - - - default domain - {/if} -
-
- {#if !data.project.enable_custom_domain} - - - Enable Custom Domain - - - - - - Are you absolutely sure? - - - Enabling a custom domain will allow you to use - your project with a custom domain. - - -
- - - {#snippet children({ props })} - Add Custom Domain -
- -
- {/snippet} -
- - - - - - Update Project Domain (leave blank to use - default) - - -

- Only include the domain name, not the - protocol. -

-

- Make sure the domain is pointing to - our server. -

-

- Please contact us if you need a custom - domain. -

-
-
-
-
- -
-
- - {#if data.cnameRecord || data.aRecord || data.aaaaRecord} - - {/if} - - - - Cancel - - - -
-
- {:else} - - {/if} -
-
-
-
- {#if data.project.enable_custom_domain} -
- - - {#snippet children({ props })} - Add Custom Domain -
- - - {#if $customDomainSubmitting} - - {/if} - Update - -
- {/snippet} -
- - - - - - Update Project Domain (leave blank to use default) - - -

- Only include the domain name, not the protocol. -

-

- Make sure the domain is pointing to our server. -

-

- Please contact us if you need a custom domain. -

-
-
-
-
- -
-
- {/if} - -
-

Settings

-

- Update project settings. -

-
- - - - - - -
-

Danger Zone

-
-
-
-
- Delete Project - - Permanently delete your project - -
- (deleteDialogOpen = open)}> - - Delete Project - - - - - Are you sure absolutely sure? - - - This action cannot be undone. This will permanently - delete your project and all its associated data. - - -
- - - {#snippet children({ props })} - -
- - - Delete Shorteners? - -
- {/snippet} -
- -
-
- - - {#if $submitting} - - {/if} - Delete - -
-
-
-
-
-
-
-
diff --git a/frontend/src/routes/(app)/dashboard/projects/[id]/settings/schema.ts b/frontend/src/routes/(app)/dashboard/projects/[id]/settings/schema.ts deleted file mode 100644 index c4d9e86..0000000 --- a/frontend/src/routes/(app)/dashboard/projects/[id]/settings/schema.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { z } from 'zod' - -export const formSchema = z.object({ - name: z.string().nonempty(), - qr_background: z - .string() - .min(1, { message: 'Background color is required' }) - .max(7), - qr_foreground: z - .string() - .min(1, { message: 'Foreground color is required' }) - .max(7), - qrCornerSquareStyle: z.custom<'dot' | 'square' | 'extra-rounded'>(), - qrDotStyle: z.custom<'square' | 'rounded'>(), - qrImage: z - .instanceof(File, { message: 'Please upload a file' }) - .refine((file) => file.size <= 2097152, `Max image size is 2MB.`) - .refine( - (file) => - file.type === 'image/jpeg' || file.type === 'image/png', - { - message: 'Only JPEG or PNG files are allowed', - }, - ) - .optional() - .nullable(), -}) - -export const enableCustomDomainFormSchema = z.object({ - enableDomain: z.string(), -}) - -export const customDomainFormSchema = z.object({ - domain: z.string(), -}) - -export type FormSchema = typeof formSchema - -export const deleteSchema = z.object({ - deleteShorteners: z.boolean(), -}) diff --git a/frontend/src/routes/(app)/dashboard/projects/schema.ts b/frontend/src/routes/(app)/dashboard/projects/schema.ts deleted file mode 100644 index 01beeb3..0000000 --- a/frontend/src/routes/(app)/dashboard/projects/schema.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { z } from 'zod' - -export const formSchema = z.object({ - name: z.string().min(1, { message: 'Name is required' }), -}) - -export type FormSchema = typeof formSchema diff --git a/frontend/src/routes/(app)/dashboard/settings/(components)/sidebar-nav.svelte b/frontend/src/routes/(app)/dashboard/settings/(components)/sidebar-nav.svelte deleted file mode 100644 index 1b2d9ec..0000000 --- a/frontend/src/routes/(app)/dashboard/settings/(components)/sidebar-nav.svelte +++ /dev/null @@ -1,47 +0,0 @@ - - - diff --git a/frontend/src/routes/(app)/dashboard/settings/+layout.server.ts b/frontend/src/routes/(app)/dashboard/settings/+layout.server.ts deleted file mode 100644 index 2e331d8..0000000 --- a/frontend/src/routes/(app)/dashboard/settings/+layout.server.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { LayoutServerLoad } from './$types' - -export const load = (async (event) => { - const { breadcrumbs: parentBreadcrumbs } = await event.parent() - - const breadcrumbs = [ - ...parentBreadcrumbs, - { name: 'Settings', path: '/dashboard/settings' }, - ] - - const page_title = 'Settings' - - return { breadcrumbs, page_title } -}) satisfies LayoutServerLoad diff --git a/frontend/src/routes/(app)/dashboard/settings/+layout.svelte b/frontend/src/routes/(app)/dashboard/settings/+layout.svelte deleted file mode 100644 index b107d5c..0000000 --- a/frontend/src/routes/(app)/dashboard/settings/+layout.svelte +++ /dev/null @@ -1,28 +0,0 @@ - - -
-
-

Settings

-

Manage your account settings.

-
- -
- -
- -
- -
-
-
-
-
-
diff --git a/frontend/src/routes/(app)/dashboard/settings/account/+page.server.ts b/frontend/src/routes/(app)/dashboard/settings/account/+page.server.ts deleted file mode 100644 index 64f645b..0000000 --- a/frontend/src/routes/(app)/dashboard/settings/account/+page.server.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { db } from '$lib/db' -import type { PageServerLoad, Actions } from './$types' -import { error, fail } from '@sveltejs/kit' -import { - message, - setError, - superValidate, -} from 'sveltekit-superforms' -import { formSchema, verifyEmailSchema } from './schema' -import { zod } from 'sveltekit-superforms/adapters' -import { user } from '$lib/db/schema' -import { eq } from 'drizzle-orm' -import { sendEmailVerification } from '$lib/server/email' - -export const load = (async (event) => { - const { username, email } = event.locals.user - - return { - form: await superValidate( - { username: username || '', email }, - zod(formSchema), - ), - verify_email_form: await superValidate(zod(verifyEmailSchema)), - } -}) satisfies PageServerLoad - -export const actions: Actions = { - update: async (event) => { - const form = await superValidate(event, zod(formSchema)) - if (!form.valid) { - return fail(400, { - form, - }) - } - - const userId = event.locals.user.id - - if (form.data.username) { - await db - .update(user) - .set({ - username: form.data.username, - }) - .where(eq(user.id, userId)) - } - - return { - form, - } - }, - verify_email: async (event) => { - const form = await superValidate(event, zod(verifyEmailSchema)) - if (!form.valid) { - return fail(400, { - form, - }) - } - - try { - await sendEmailVerification({ - userId: event.locals.user.id, - email: event.locals.user.email, - }) - } catch (e) { - return message(form, 'Error sending email verification', { - status: 500, - }) - } - - return message(form, 'Email verification sent') - }, -} diff --git a/frontend/src/routes/(app)/dashboard/settings/account/+page.svelte b/frontend/src/routes/(app)/dashboard/settings/account/+page.svelte deleted file mode 100644 index 833f533..0000000 --- a/frontend/src/routes/(app)/dashboard/settings/account/+page.svelte +++ /dev/null @@ -1,118 +0,0 @@ - - -
-
-

Account

-

- Update your account settings. -

-
- - -
- - - {#snippet children({ props })} - Username - - {/snippet} - - Change Your Username - - - - - {#snippet children({ props })} - - Email - - {#if data.user.email_verified} - (verified) - {:else} - (unverified) - {/if} - - -
- - -
- {/snippet} -
- Change Your Email - -
- - {#if $submitting} - - {/if} - Save - -
-
diff --git a/frontend/src/routes/(app)/dashboard/settings/account/schema.ts b/frontend/src/routes/(app)/dashboard/settings/account/schema.ts deleted file mode 100644 index 9c3c241..0000000 --- a/frontend/src/routes/(app)/dashboard/settings/account/schema.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { z } from 'zod' - -export const formSchema = z.object({ - username: z.string().optional(), - email: z.string().email().optional(), -}) - -export const verifyEmailSchema = z.object({}) - -export type FormSchema = typeof formSchema diff --git a/frontend/src/routes/(app)/dashboard/settings/qr/(components)/DemoQR.svelte b/frontend/src/routes/(app)/dashboard/settings/qr/(components)/DemoQR.svelte deleted file mode 100644 index aaed3e0..0000000 --- a/frontend/src/routes/(app)/dashboard/settings/qr/(components)/DemoQR.svelte +++ /dev/null @@ -1,88 +0,0 @@ - - -{value} diff --git a/frontend/src/routes/(app)/dashboard/settings/qr/+page.server.ts b/frontend/src/routes/(app)/dashboard/settings/qr/+page.server.ts deleted file mode 100644 index 9af6a54..0000000 --- a/frontend/src/routes/(app)/dashboard/settings/qr/+page.server.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { db } from '$lib/db' -import type { PageServerLoad, Actions } from './$types' -import { fail } from '@sveltejs/kit' -import { superValidate, withFiles } from 'sveltekit-superforms' -import { formSchema } from './schema' -import { zod } from 'sveltekit-superforms/adapters' -import { eq } from 'drizzle-orm' -import { user as userTable } from '$lib/db/schema' - -export const load = (async (event) => { - const user = event.locals.user - - const qr_background = user.qrBackground - const qr_foreground = user.qrForeground - - return { - qrImageBase64: user.qrImageBase64, - form: await superValidate( - { - qr_background, - qr_foreground, - qrCornerSquareStyle: user.qrCornerSquareStyle, - qrDotStyle: user.qrDotStyle, - }, - zod(formSchema), - { errors: false }, - ), - } -}) satisfies PageServerLoad - -export const actions: Actions = { - default: async (event) => { - const form = await superValidate(event, zod(formSchema)) - if (!form.valid) { - return fail( - 400, - withFiles({ - form, - }), - ) - } - - const user = event.locals.user - const userId = event.locals.user.id - - const { - qr_background, - qr_foreground, - qrCornerSquareStyle, - qrDotStyle, - qrImage, - } = form.data - - const qrImageType = qrImage ? qrImage.type : undefined - const qrImageBlob = qrImage - ? await qrImage.arrayBuffer() - : undefined - const qrImageBase64 = qrImageBlob - ? Buffer.from(qrImageBlob).toString('base64') - : undefined - - await db - .update(userTable) - .set({ - qrBackground: qr_background, - qrForeground: qr_foreground, - }) - .where(eq(userTable.id, userId)) - - if (user.plan !== 'free') { - await db - .update(userTable) - .set({ - qrCornerSquareStyle, - qrDotStyle, - qrImageBase64: qrImage - ? `data:${qrImageType};base64,${qrImageBase64}` - : undefined, - }) - .where(eq(userTable.id, userId)) - } - - return withFiles({ - form, - }) - }, -} diff --git a/frontend/src/routes/(app)/dashboard/settings/qr/+page.svelte b/frontend/src/routes/(app)/dashboard/settings/qr/+page.svelte deleted file mode 100644 index c8471c7..0000000 --- a/frontend/src/routes/(app)/dashboard/settings/qr/+page.svelte +++ /dev/null @@ -1,237 +0,0 @@ - - -
-
-

QR

-

- Update your QR settings. -

-
- - -
- -
- -
- - - {#snippet children({ props })} - Background Color - - {/snippet} - - QR Code background color - - - - - {#snippet children({ props })} - Foreground Color - - {/snippet} - - QR Code foreground color - - - - - {#snippet children({ props })} - - Image (Pro) - -
- {#if !$formData.qrImage && !data.qrImageBase64} - - {:else} - - {/if} -
- Click to edit - { - const file = e.currentTarget.files?.item(0) - if (!file) return - - if (file.size > 2097152) { - toast.error('Too Big! Max file size is 2MB') - return - } - $formData.qrImage = file - }} /> - {/snippet} -
- -
- - - {#snippet children({ props })} - - Corner Square Style (Pro) - - - {/snippet} - -
- - - -
-
- - - {#snippet children({ props })} - - Dot Style (Pro) - - - {/snippet} - -
- - -
-
- - - {#if $submitting} - - {/if} - Save - -
-
diff --git a/frontend/src/routes/(app)/dashboard/settings/qr/schema.ts b/frontend/src/routes/(app)/dashboard/settings/qr/schema.ts deleted file mode 100644 index ecbfd5a..0000000 --- a/frontend/src/routes/(app)/dashboard/settings/qr/schema.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { z } from 'zod' - -export const formSchema = z.object({ - qr_background: z - .string() - .min(1, { message: 'Background color is required' }) - .max(7), - qr_foreground: z - .string() - .min(1, { message: 'Foreground color is required' }) - .max(7), - qrCornerSquareStyle: z.custom<'dot' | 'square' | 'extra-rounded'>(), - qrDotStyle: z.custom<'square' | 'rounded'>(), - qrImage: z - .instanceof(File, { message: 'Please upload a file' }) - .refine((file) => file.size <= 2097152, `Max image size is 2MB.`) - .refine( - (file) => - file.type === 'image/jpeg' || file.type === 'image/png', - { - message: 'Only JPEG or PNG files are allowed', - }, - ) - .optional() - .nullable(), -}) diff --git a/frontend/src/routes/(app)/dashboard/settings/security/+page.server.ts b/frontend/src/routes/(app)/dashboard/settings/security/+page.server.ts deleted file mode 100644 index f505b05..0000000 --- a/frontend/src/routes/(app)/dashboard/settings/security/+page.server.ts +++ /dev/null @@ -1,171 +0,0 @@ -import { db } from '$lib/db' -import type { PageServerLoad, Actions } from './$types' -import { fail } from '@sveltejs/kit' -import { setError, superValidate } from 'sveltekit-superforms' -import { - changePasswordFormSchema, - deleteAccountSchema, -} from './schema' -import { zod } from 'sveltekit-superforms/adapters' -import { project, shortener, user, visitor } from '$lib/db/schema' -import { eq, inArray } from 'drizzle-orm' -import { lucia } from '$lib/server/auth' -import { env } from '$env/dynamic/private' -import * as argon2 from 'argon2' - -export const load = (async (event) => { - return { - form: await superValidate(zod(changePasswordFormSchema)), - deleteAccountForm: await superValidate(zod(deleteAccountSchema)), - } -}) satisfies PageServerLoad - -export const actions: Actions = { - change_password: async (event) => { - const form = await superValidate( - event, - zod(changePasswordFormSchema), - ) - if (!form.valid) { - return fail(400, { - form, - }) - } - - const userId = event.locals.user.id - - if (event.locals.user.googleId) { - return setError( - form, - 'old_password', - 'Unable to set a password if using google login', - ) - } - - const userData = await db.query.user.findFirst({ - where: (user, { eq }) => eq(user.id, userId), - }) - - if (!userData) { - return setError(form, 'old_password', 'User Not Found') - } - - if (!userData.password) { - return setError( - form, - 'old_password', - 'User is using other login method', - ) - } - - const passwordMatch = await argon2.verify( - userData.password, - form.data.old_password, - ) - - if (!passwordMatch) { - return setError(form, 'old_password', 'Old Password Not Match') - } - - const newPassword = await argon2.hash(form.data.new_password) - - await db - .update(user) - .set({ - password: newPassword, - }) - .where(eq(user.id, userId)) - - await lucia.invalidateUserSessions(userId) - - const session = await lucia.createSession(userId, {}) - const sessionCookie = lucia.createSessionCookie(session.id) - - event.cookies.set(sessionCookie.name, sessionCookie.value, { - ...sessionCookie.attributes, - path: '/', - secure: env.APP_ENV === 'prod', - }) - - return { - form, - } - }, - delete_account: async (event) => { - const form = await superValidate(event, zod(deleteAccountSchema)) - if (!form.valid) { - return fail(400, { - form, - }) - } - - const userId = event.locals.user.id - - const userData = await db.query.user.findFirst({ - where: (user, { eq }) => eq(user.id, userId), - }) - - if (!userData) { - return setError(form, 'password', 'User Not Found') - } - - if (userData.googleId) { - await lucia.invalidateUserSessions(userId) - - await db.delete(user).where(eq(user.id, userId)) - - const shorteners = await db - .delete(shortener) - .where(eq(shortener.userId, userId)) - .returning() - - await db.delete(project).where(eq(project.userId, userId)) - - await db.delete(visitor).where( - inArray( - visitor.shortenerId, - shorteners.map((shortener) => shortener.id), - ), - ) - - return { - form, - } - } - - if (!userData.password) { - return setError(form, 'password', 'User Not Found') - } - - const passwordMatch = await argon2.verify( - userData.password, - form.data.password, - ) - - if (!passwordMatch) { - return setError(form, 'password', 'Invalid Password') - } - - await lucia.invalidateUserSessions(userId) - - await db.delete(user).where(eq(user.id, userId)) - - const shorteners = await db - .delete(shortener) - .where(eq(shortener.userId, userId)) - .returning() - - await db.delete(project).where(eq(project.userId, userId)) - - await db.delete(visitor).where( - inArray( - visitor.shortenerId, - shorteners.map((shortener) => shortener.id), - ), - ) - - return { - form, - } - }, -} diff --git a/frontend/src/routes/(app)/dashboard/settings/security/+page.svelte b/frontend/src/routes/(app)/dashboard/settings/security/+page.svelte deleted file mode 100644 index 34c9e6c..0000000 --- a/frontend/src/routes/(app)/dashboard/settings/security/+page.svelte +++ /dev/null @@ -1,190 +0,0 @@ - - -
-
-

Password

-

Update your password

-
- - -
- - - {#snippet children({ props })} - Old Password - - {/snippet} - - Old Password To Confirm - - - - - {#snippet children({ props })} - New Password - - {/snippet} - - Update Password - - - - - {#snippet children({ props })} - Confirm Password - - {/snippet} - - Confirm New Password - - - - {#if $submitting} - - {/if} - Change Password - -
- - - -
-

Danger Zone

-

- Changes here are irreversible -

-
- - -
-
-
- Delete Account - - Permanently delete your account and all data - -
- - - Delete Account - - - -
- -
-
- Delete Account - - This will permanently delete your account and all its - associated data. - -
-
-
- - - {#snippet children({ props })} - Password - - {/snippet} - - - Enter your password to delete account - - - -
- - - {#if $deleteAccountSubmitting} - - {/if} - Delete - -
-
-
-
-
-
-
diff --git a/frontend/src/routes/(app)/dashboard/settings/security/schema.ts b/frontend/src/routes/(app)/dashboard/settings/security/schema.ts deleted file mode 100644 index f47fed5..0000000 --- a/frontend/src/routes/(app)/dashboard/settings/security/schema.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { z } from 'zod' - -export const changePasswordFormSchema = z - .object({ - old_password: z.string(), - new_password: z.string().min(8, { - message: 'Password must be at least 8 characters long', - }), - confirm_password: z.string().min(8, { - message: 'Password must be at least 8 characters long', - }), - }) - .refine( - (data) => data.new_password == data.confirm_password, - "Passwords didn't match.", - ) - -export const verifyEmailSchema = z.object({}) - -export const deleteAccountSchema = z.object({ password: z.string() }) - -export type FormSchema = typeof changePasswordFormSchema diff --git a/frontend/src/routes/(public)/(landing)/+layout.svelte b/frontend/src/routes/(public)/(landing)/+layout.svelte index b0dac76..71eeb59 100644 --- a/frontend/src/routes/(public)/(landing)/+layout.svelte +++ b/frontend/src/routes/(public)/(landing)/+layout.svelte @@ -21,12 +21,7 @@ @@ -46,7 +41,7 @@