auth + player
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,15 @@
|
|||||||
|
import type { Config } from 'drizzle-kit';
|
||||||
|
import { config } from 'dotenv';
|
||||||
|
config();
|
||||||
|
|
||||||
|
export default {
|
||||||
|
schema: './src/lib/db/schema.ts',
|
||||||
|
out: './drizzle',
|
||||||
|
driver: 'pg', // 'pg' | 'mysql2' | 'better-sqlite' | 'libsql' | 'turso'
|
||||||
|
dbCredentials: {
|
||||||
|
host: process.env.DB_HOST || "",
|
||||||
|
user: process.env.DB_USER,
|
||||||
|
password: process.env.DB_PASSWORD,
|
||||||
|
database: process.env.DB_NAME || "vocalcast"
|
||||||
|
}
|
||||||
|
} satisfies Config;
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS "pods" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"user_id" uuid,
|
||||||
|
"script" text NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE IF NOT EXISTS "sessions" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"user_id" uuid NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE IF NOT EXISTS "users" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"name" text NOT NULL,
|
||||||
|
"email" text NOT NULL,
|
||||||
|
"hashed_password" text NOT NULL,
|
||||||
|
CONSTRAINT "users_email_unique" UNIQUE("email")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "pods" ADD CONSTRAINT "pods_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE no action ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "pods" ADD COLUMN "date_created" date DEFAULT now() NOT NULL;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE "sessions" ADD COLUMN "date_created" date DEFAULT now() NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "users" ADD COLUMN "date_created" date DEFAULT now() NOT NULL;
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
{
|
||||||
|
"id": "574b847a-680e-45d7-b245-e0bd724e0419",
|
||||||
|
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||||
|
"version": "5",
|
||||||
|
"dialect": "pg",
|
||||||
|
"tables": {
|
||||||
|
"pods": {
|
||||||
|
"name": "pods",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"script": {
|
||||||
|
"name": "script",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"pods_user_id_users_id_fk": {
|
||||||
|
"name": "pods_user_id_users_id_fk",
|
||||||
|
"tableFrom": "pods",
|
||||||
|
"tableTo": "users",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
},
|
||||||
|
"sessions": {
|
||||||
|
"name": "sessions",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
},
|
||||||
|
"users": {
|
||||||
|
"name": "users",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"hashed_password": {
|
||||||
|
"name": "hashed_password",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"users_email_unique": {
|
||||||
|
"name": "users_email_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"email"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {},
|
||||||
|
"schemas": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
{
|
||||||
|
"id": "2d17f61e-4b66-478c-8e89-d00feb7cd483",
|
||||||
|
"prevId": "574b847a-680e-45d7-b245-e0bd724e0419",
|
||||||
|
"version": "5",
|
||||||
|
"dialect": "pg",
|
||||||
|
"tables": {
|
||||||
|
"pods": {
|
||||||
|
"name": "pods",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"script": {
|
||||||
|
"name": "script",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"date_created": {
|
||||||
|
"name": "date_created",
|
||||||
|
"type": "date",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"pods_user_id_users_id_fk": {
|
||||||
|
"name": "pods_user_id_users_id_fk",
|
||||||
|
"tableFrom": "pods",
|
||||||
|
"tableTo": "users",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
},
|
||||||
|
"sessions": {
|
||||||
|
"name": "sessions",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
},
|
||||||
|
"users": {
|
||||||
|
"name": "users",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"hashed_password": {
|
||||||
|
"name": "hashed_password",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"users_email_unique": {
|
||||||
|
"name": "users_email_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"email"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {},
|
||||||
|
"schemas": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
{
|
||||||
|
"id": "c9330d3e-bd77-4396-994a-a975cf5e4b72",
|
||||||
|
"prevId": "2d17f61e-4b66-478c-8e89-d00feb7cd483",
|
||||||
|
"version": "5",
|
||||||
|
"dialect": "pg",
|
||||||
|
"tables": {
|
||||||
|
"pods": {
|
||||||
|
"name": "pods",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"script": {
|
||||||
|
"name": "script",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"date_created": {
|
||||||
|
"name": "date_created",
|
||||||
|
"type": "date",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"pods_user_id_users_id_fk": {
|
||||||
|
"name": "pods_user_id_users_id_fk",
|
||||||
|
"tableFrom": "pods",
|
||||||
|
"tableTo": "users",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
},
|
||||||
|
"sessions": {
|
||||||
|
"name": "sessions",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"date_created": {
|
||||||
|
"name": "date_created",
|
||||||
|
"type": "date",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
},
|
||||||
|
"users": {
|
||||||
|
"name": "users",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"hashed_password": {
|
||||||
|
"name": "hashed_password",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"date_created": {
|
||||||
|
"name": "date_created",
|
||||||
|
"type": "date",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"users_email_unique": {
|
||||||
|
"name": "users_email_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"email"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {},
|
||||||
|
"schemas": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"version": "5",
|
||||||
|
"dialect": "pg",
|
||||||
|
"entries": [
|
||||||
|
{
|
||||||
|
"idx": 0,
|
||||||
|
"version": "5",
|
||||||
|
"when": 1710166129473,
|
||||||
|
"tag": "0000_lonely_moonstone",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 1,
|
||||||
|
"version": "5",
|
||||||
|
"when": 1710166412116,
|
||||||
|
"tag": "0001_married_nomad",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 2,
|
||||||
|
"version": "5",
|
||||||
|
"when": 1710167109396,
|
||||||
|
"tag": "0002_zippy_the_hunter",
|
||||||
|
"breakpoints": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Generated
+2083
-94
File diff suppressed because it is too large
Load Diff
+11
-2
@@ -15,7 +15,9 @@
|
|||||||
"@sveltejs/adapter-auto": "^3.0.0",
|
"@sveltejs/adapter-auto": "^3.0.0",
|
||||||
"@sveltejs/kit": "^2.0.0",
|
"@sveltejs/kit": "^2.0.0",
|
||||||
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
||||||
|
"@types/bcrypt": "^5.0.2",
|
||||||
"autoprefixer": "^10.4.16",
|
"autoprefixer": "^10.4.16",
|
||||||
|
"drizzle-kit": "^0.20.14",
|
||||||
"postcss": "^8.4.32",
|
"postcss": "^8.4.32",
|
||||||
"postcss-load-config": "^5.0.2",
|
"postcss-load-config": "^5.0.2",
|
||||||
"prettier": "^3.1.1",
|
"prettier": "^3.1.1",
|
||||||
@@ -30,10 +32,17 @@
|
|||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bits-ui": "^0.19.5",
|
"bcrypt": "^5.1.1",
|
||||||
|
"bits-ui": "^0.19.6",
|
||||||
"clsx": "^2.1.0",
|
"clsx": "^2.1.0",
|
||||||
|
"dotenv": "^16.4.5",
|
||||||
|
"drizzle-orm": "^0.30.1",
|
||||||
|
"formsnap": "^0.5.1",
|
||||||
"lucide-svelte": "^0.354.0",
|
"lucide-svelte": "^0.354.0",
|
||||||
|
"pg": "^8.11.3",
|
||||||
|
"sveltekit-superforms": "^2.8.1",
|
||||||
"tailwind-merge": "^2.2.1",
|
"tailwind-merge": "^2.2.1",
|
||||||
"tailwind-variants": "^0.2.0"
|
"tailwind-variants": "^0.2.0",
|
||||||
|
"zod": "^3.22.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-1
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
:root {
|
:root {
|
||||||
--background: 25 70% 98%;
|
--background: rgb(253, 249, 246);
|
||||||
--foreground: 25 67% 4%;
|
--foreground: 25 67% 4%;
|
||||||
--muted: 25 30% 95%;
|
--muted: 25 30% 95%;
|
||||||
--muted-foreground: 25 2% 29%;
|
--muted-foreground: 25 2% 29%;
|
||||||
@@ -60,3 +60,13 @@
|
|||||||
.font-epica {
|
.font-epica {
|
||||||
font-family: Epica ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
|
font-family: Epica ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.scrollbars-hidden::-webkit-scrollbar {
|
||||||
|
background: transparent; /* Chrome/Safari/Webkit */
|
||||||
|
width: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollbars-hidden {
|
||||||
|
scrollbar-width: none; /* Firefox */
|
||||||
|
-ms-overflow-style: none; /* IE 10+ */
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { validateSession } from '$lib/services/auth.server';
|
||||||
|
|
||||||
|
/** @type {import('@sveltejs/kit').Handle} */
|
||||||
|
export async function handle({ event, resolve }) {
|
||||||
|
const session_id = event.cookies.get("token");
|
||||||
|
if (session_id === undefined && event.url.pathname !== "/auth") return Response.redirect(event.url.host+"/auth", 303);
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const user = await validateSession(session_id);
|
||||||
|
|
||||||
|
if(!user) return Response.redirect(event.url.host+"/auth", 303);
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
event.locals.user = user;
|
||||||
|
|
||||||
|
return resolve(event);
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import * as Form from "$lib/components/ui/form";
|
||||||
|
import { Input } from "$lib/components/ui/input";
|
||||||
|
import { Loader2 } from "lucide-svelte";
|
||||||
|
import { formSchema, type FormSchema } from "./schema";
|
||||||
|
import {
|
||||||
|
type SuperValidated,
|
||||||
|
type Infer,
|
||||||
|
superForm,
|
||||||
|
} from "sveltekit-superforms";
|
||||||
|
import { zodClient } from "sveltekit-superforms/adapters";
|
||||||
|
|
||||||
|
let isLoading = false;
|
||||||
|
|
||||||
|
export let data: SuperValidated<Infer<FormSchema>>;
|
||||||
|
|
||||||
|
const form = superForm(data, {
|
||||||
|
validators: zodClient(formSchema),
|
||||||
|
onSubmit: () => {
|
||||||
|
isLoading = true;
|
||||||
|
},
|
||||||
|
onUpdated: ({form: f}) => {
|
||||||
|
isLoading = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const { form: formData, enhance, message } = form;
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<form method="POST" action="?/signup" use:enhance>
|
||||||
|
{#if $message}
|
||||||
|
<span class="block text-center text-red-600">{$message}</span>
|
||||||
|
{/if}
|
||||||
|
<Form.Field {form} name="name">
|
||||||
|
<Form.Control let:attrs>
|
||||||
|
<Form.Label>Name</Form.Label>
|
||||||
|
<Input
|
||||||
|
{...attrs}
|
||||||
|
bind:value={$formData.name}
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
</Form.Control>
|
||||||
|
<Form.FieldErrors />
|
||||||
|
</Form.Field>
|
||||||
|
<Form.Field {form} name="email">
|
||||||
|
<Form.Control let:attrs>
|
||||||
|
<Form.Label>Email</Form.Label>
|
||||||
|
<Input
|
||||||
|
{...attrs}
|
||||||
|
type="email"
|
||||||
|
bind:value={$formData.email}
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
</Form.Control>
|
||||||
|
<Form.FieldErrors />
|
||||||
|
</Form.Field>
|
||||||
|
|
||||||
|
<Form.Field {form} name="password">
|
||||||
|
<Form.Control let:attrs>
|
||||||
|
<Form.Label>Password</Form.Label>
|
||||||
|
<Input
|
||||||
|
{...attrs}
|
||||||
|
type="password"
|
||||||
|
bind:value={$formData.password}
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
</Form.Control>
|
||||||
|
<Form.Description>A strong password with 6-20 characters.</Form.Description>
|
||||||
|
<Form.FieldErrors />
|
||||||
|
</Form.Field>
|
||||||
|
<Form.Button disabled={isLoading} class="w-full">
|
||||||
|
{#if isLoading}
|
||||||
|
<Loader2 class="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
{:else}
|
||||||
|
Submit
|
||||||
|
{/if}
|
||||||
|
</Form.Button>
|
||||||
|
</form>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const formSchema = z.object({
|
||||||
|
name: z.string().min(2).max(16),
|
||||||
|
email: z.string().min(2).max(50).email("Invalid email format"),
|
||||||
|
password: z.string().min(6).max(20),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type FormSchema = typeof formSchema;
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
<script>
|
||||||
|
import { browser } from '$app/environment';
|
||||||
|
import { ArrowLeft, PauseIcon, Play } from 'lucide-svelte';
|
||||||
|
import { createEventDispatcher, onMount } from 'svelte';
|
||||||
|
import {Progress} from '$lib/components/ui/progress';
|
||||||
|
/** @type {string} */
|
||||||
|
export let script;
|
||||||
|
|
||||||
|
/** @type {SpeechSynthesisUtterance} */
|
||||||
|
let utterance;
|
||||||
|
let paused = false;
|
||||||
|
let progress = 0;
|
||||||
|
/** @type {number} */
|
||||||
|
let wordIndex = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {SpeechSynthesis}
|
||||||
|
*/
|
||||||
|
let speechSynthesis;
|
||||||
|
|
||||||
|
let dispatcher = createEventDispatcher();
|
||||||
|
|
||||||
|
function ready() {
|
||||||
|
speechSynthesis.cancel();
|
||||||
|
|
||||||
|
const text = script;
|
||||||
|
const words = text.split(/\s+/);
|
||||||
|
utterance = new SpeechSynthesisUtterance(text);
|
||||||
|
utterance.addEventListener('boundary', (event) => {
|
||||||
|
if (event.name === 'word') {
|
||||||
|
console.log(words[wordIndex]);
|
||||||
|
document.querySelector(`span[data-index="${wordIndex}"]`)?.classList.add('text-black');
|
||||||
|
document.querySelector(`span[data-index="${wordIndex}"]`)?.scrollIntoView({
|
||||||
|
behavior: 'smooth',
|
||||||
|
block: 'center',
|
||||||
|
inline: 'center'
|
||||||
|
});
|
||||||
|
wordIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
progress = event.charIndex;
|
||||||
|
});
|
||||||
|
|
||||||
|
utterance.addEventListener('start', (event) => {
|
||||||
|
paused = false;
|
||||||
|
|
||||||
|
// const length = words.length;
|
||||||
|
// const updateProgress = () => {
|
||||||
|
// progress = Math.round((event.charIndex / length) * 100);
|
||||||
|
// if (!paused) {
|
||||||
|
// requestAnimationFrame(updateProgress);
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
// updateProgress();
|
||||||
|
});
|
||||||
|
|
||||||
|
utterance.addEventListener('pause', () => {
|
||||||
|
paused = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
utterance.addEventListener('resume', () => {
|
||||||
|
paused = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
utterance.addEventListener('end', () => {
|
||||||
|
progress = 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
speechSynthesis.speak(utterance);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pauseOrResumeUtterance = () => {
|
||||||
|
if (!speechSynthesis) return;
|
||||||
|
console.log('pause or resume');
|
||||||
|
if (!speechSynthesis.speaking) {
|
||||||
|
ready();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (paused) {
|
||||||
|
console.log('resume');
|
||||||
|
speechSynthesis.resume();
|
||||||
|
} else {
|
||||||
|
console.log('pause');
|
||||||
|
speechSynthesis.pause();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
if (browser && 'speechSynthesis' in window) {
|
||||||
|
speechSynthesis = window.speechSynthesis;
|
||||||
|
} else {
|
||||||
|
alert(
|
||||||
|
'Sorry your browser <strong>does not support</strong> speech synthesis.<br>Try this in <a href="https://www.google.co.uk/intl/en/chrome/browser/canary.html">Chrome Canary</a>.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="absolute inset-0 flex h-full w-full bg-white">
|
||||||
|
<div
|
||||||
|
class="to-[hsl(rgb(253, 249, 246) / 1)] flex flex-col gap-16 bg-gradient-to-t from-orange-100 from-50% to-background to-80% p-10"
|
||||||
|
>
|
||||||
|
<button on:click={() => dispatcher('close')}>
|
||||||
|
<ArrowLeft class="h-8 w-8" />
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
class="text-container script scrollbars-hidden h-[80%] overflow-scroll text-2xl pb-8"
|
||||||
|
style="line-height: 3rem; color:rgba(0,0,0,0.5)"
|
||||||
|
>
|
||||||
|
{#each script.split(/\s+/) as word, i}
|
||||||
|
<span data-index={i}>{word} </span>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
<Progress class="w-full" max={script.length} value={progress} />
|
||||||
|
<button
|
||||||
|
class="m-0 mx-auto flex aspect-square w-12 items-center justify-center rounded-full bg-white p-0"
|
||||||
|
on:click={pauseOrResumeUtterance}
|
||||||
|
>
|
||||||
|
{#if paused}
|
||||||
|
<Play class="h-6 w-6" />
|
||||||
|
{:else}
|
||||||
|
<PauseIcon class="h-6 w-6" />
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.text-container {
|
||||||
|
--mask: linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
rgba(0, 0, 0, 1) 0,
|
||||||
|
rgba(0, 0, 0, 1) 80%,
|
||||||
|
rgba(0, 0, 0, 0) 95%,
|
||||||
|
rgba(0, 0, 0, 0) 0
|
||||||
|
)
|
||||||
|
100% 50% / 100% 100% repeat-x;
|
||||||
|
|
||||||
|
-webkit-mask: var(--mask);
|
||||||
|
mask: var(--mask);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import * as Button from "$lib/components/ui/button/index.js";
|
||||||
|
|
||||||
|
type $$Props = Button.Props;
|
||||||
|
type $$Events = Button.Events;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Button.Root type="submit" on:click on:keydown {...$$restProps}>
|
||||||
|
<slot />
|
||||||
|
</Button.Root>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import * as FormPrimitive from "formsnap";
|
||||||
|
import { cn } from "$lib/utils.js.js";
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
|
||||||
|
type $$Props = HTMLAttributes<HTMLSpanElement>;
|
||||||
|
let className: string | undefined | null = undefined;
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<FormPrimitive.Description
|
||||||
|
class={cn("text-sm text-muted-foreground", className)}
|
||||||
|
{...$$restProps}
|
||||||
|
let:descriptionAttrs
|
||||||
|
>
|
||||||
|
<slot {descriptionAttrs} />
|
||||||
|
</FormPrimitive.Description>
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<script lang="ts" context="module">
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
import type { FormPathLeaves, SuperForm } from "sveltekit-superforms";
|
||||||
|
type T = Record<string, unknown>;
|
||||||
|
type U = unknown;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts" generics="T extends Record<string, unknown>, U extends FormPathLeaves<T>">
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import * as FormPrimitive from "formsnap";
|
||||||
|
import { cn } from "$lib/utils.js.js";
|
||||||
|
|
||||||
|
type $$Props = FormPrimitive.ElementFieldProps<T, U> & HTMLAttributes<HTMLElement>;
|
||||||
|
|
||||||
|
export let form: SuperForm<T>;
|
||||||
|
export let name: U;
|
||||||
|
|
||||||
|
let className: $$Props["class"] = undefined;
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<FormPrimitive.ElementField {form} {name} let:constraints let:errors let:tainted let:value>
|
||||||
|
<div class={cn("space-y-2", className)}>
|
||||||
|
<slot {constraints} {errors} {tainted} {value} />
|
||||||
|
</div>
|
||||||
|
</FormPrimitive.ElementField>
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import * as FormPrimitive from "formsnap";
|
||||||
|
import { cn } from "$lib/utils.js.js";
|
||||||
|
|
||||||
|
type $$Props = FormPrimitive.FieldErrorsProps & {
|
||||||
|
errorClasses?: string | undefined | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
let className: $$Props["class"] = undefined;
|
||||||
|
export { className as class };
|
||||||
|
export let errorClasses: $$Props["class"] = undefined;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<FormPrimitive.FieldErrors
|
||||||
|
class={cn("text-sm font-medium text-destructive", className)}
|
||||||
|
{...$$restProps}
|
||||||
|
let:errors
|
||||||
|
let:fieldErrorsAttrs
|
||||||
|
let:errorAttrs
|
||||||
|
>
|
||||||
|
<slot {errors} {fieldErrorsAttrs} {errorAttrs}>
|
||||||
|
{#each errors as error}
|
||||||
|
<div {...errorAttrs} class={cn(errorClasses)}>{error}</div>
|
||||||
|
{/each}
|
||||||
|
</slot>
|
||||||
|
</FormPrimitive.FieldErrors>
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<script lang="ts" context="module">
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
import type { FormPath, SuperForm } from "sveltekit-superforms";
|
||||||
|
type T = Record<string, unknown>;
|
||||||
|
type U = unknown;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts" generics="T extends Record<string, unknown>, U extends FormPath<T>">
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import * as FormPrimitive from "formsnap";
|
||||||
|
import { cn } from "$lib/utils.js.js";
|
||||||
|
|
||||||
|
type $$Props = FormPrimitive.FieldProps<T, U> & HTMLAttributes<HTMLElement>;
|
||||||
|
|
||||||
|
export let form: SuperForm<T>;
|
||||||
|
export let name: U;
|
||||||
|
|
||||||
|
let className: $$Props["class"] = undefined;
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<FormPrimitive.Field {form} {name} let:constraints let:errors let:tainted let:value>
|
||||||
|
<div class={cn("space-y-2", className)}>
|
||||||
|
<slot {constraints} {errors} {tainted} {value} />
|
||||||
|
</div>
|
||||||
|
</FormPrimitive.Field>
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<script lang="ts" context="module">
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
import type { FormPath, SuperForm } from "sveltekit-superforms";
|
||||||
|
type T = Record<string, unknown>;
|
||||||
|
type U = unknown;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts" generics="T extends Record<string, unknown>, U extends FormPath<T>">
|
||||||
|
import * as FormPrimitive from "formsnap";
|
||||||
|
import { cn } from "$lib/utils.js.js";
|
||||||
|
|
||||||
|
type $$Props = FormPrimitive.FieldsetProps<T, U>;
|
||||||
|
|
||||||
|
export let form: SuperForm<T>;
|
||||||
|
export let name: U;
|
||||||
|
|
||||||
|
let className: $$Props["class"] = undefined;
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<FormPrimitive.Fieldset
|
||||||
|
{form}
|
||||||
|
{name}
|
||||||
|
let:constraints
|
||||||
|
let:errors
|
||||||
|
let:tainted
|
||||||
|
let:value
|
||||||
|
class={cn("space-y-2", className)}
|
||||||
|
>
|
||||||
|
<slot {constraints} {errors} {tainted} {value} />
|
||||||
|
</FormPrimitive.Fieldset>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Label as LabelPrimitive } from "bits-ui";
|
||||||
|
import { getFormControl } from "formsnap";
|
||||||
|
import { cn } from "$lib/utils.js.js";
|
||||||
|
import { Label } from "$lib/components/ui/label/index.js";
|
||||||
|
|
||||||
|
type $$Props = LabelPrimitive.Props;
|
||||||
|
|
||||||
|
let className: $$Props["class"] = undefined;
|
||||||
|
export { className as class };
|
||||||
|
|
||||||
|
const { labelAttrs } = getFormControl();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Label {...$labelAttrs} class={cn("data-[fs-error]:text-destructive", className)} {...$$restProps}>
|
||||||
|
<slot {labelAttrs} />
|
||||||
|
</Label>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import * as FormPrimitive from "formsnap";
|
||||||
|
import { cn } from "$lib/utils.js.js";
|
||||||
|
|
||||||
|
type $$Props = FormPrimitive.LegendProps;
|
||||||
|
|
||||||
|
let className: $$Props["class"] = undefined;
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<FormPrimitive.Legend
|
||||||
|
{...$$restProps}
|
||||||
|
class={cn("text-sm font-medium leading-none data-[fs-error]:text-destructive", className)}
|
||||||
|
let:legendAttrs
|
||||||
|
>
|
||||||
|
<slot {legendAttrs} />
|
||||||
|
</FormPrimitive.Legend>
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import * as FormPrimitive from "formsnap";
|
||||||
|
import Description from "./form-description.svelte";
|
||||||
|
import Label from "./form-label.svelte";
|
||||||
|
import FieldErrors from "./form-field-errors.svelte";
|
||||||
|
import Field from "./form-field.svelte";
|
||||||
|
import Fieldset from "./form-fieldset.svelte";
|
||||||
|
import Legend from "./form-legend.svelte";
|
||||||
|
import ElementField from "./form-element-field.svelte";
|
||||||
|
import Button from "./form-button.svelte";
|
||||||
|
|
||||||
|
const Control = FormPrimitive.Control;
|
||||||
|
|
||||||
|
export {
|
||||||
|
Field,
|
||||||
|
Control,
|
||||||
|
Label,
|
||||||
|
Button,
|
||||||
|
FieldErrors,
|
||||||
|
Description,
|
||||||
|
Fieldset,
|
||||||
|
Legend,
|
||||||
|
ElementField,
|
||||||
|
//
|
||||||
|
Field as FormField,
|
||||||
|
Control as FormControl,
|
||||||
|
Description as FormDescription,
|
||||||
|
Label as FormLabel,
|
||||||
|
FieldErrors as FormFieldErrors,
|
||||||
|
Fieldset as FormFieldset,
|
||||||
|
Legend as FormLegend,
|
||||||
|
ElementField as FormElementField,
|
||||||
|
Button as FormButton,
|
||||||
|
};
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import Root from "./progress.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
//
|
||||||
|
Root as Progress,
|
||||||
|
};
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Progress as ProgressPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js.js";
|
||||||
|
|
||||||
|
type $$Props = ProgressPrimitive.Props;
|
||||||
|
|
||||||
|
let className: $$Props["class"] = undefined;
|
||||||
|
export let max: $$Props["max"] = 100;
|
||||||
|
export let value: $$Props["value"] = undefined;
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ProgressPrimitive.Root
|
||||||
|
class={cn("relative h-4 w-full overflow-hidden rounded-full bg-secondary", className)}
|
||||||
|
{...$$restProps}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="h-full w-full flex-1 bg-primary transition-all"
|
||||||
|
style={`transform: translateX(-${100 - (100 * (value ?? 0)) / (max ?? 1)}%)`}
|
||||||
|
/>
|
||||||
|
</ProgressPrimitive.Root>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { config } from 'dotenv';
|
||||||
|
import { drizzle } from 'drizzle-orm/postgres-js';
|
||||||
|
import postgres from 'postgres';
|
||||||
|
config();
|
||||||
|
|
||||||
|
// There are multiple ways to initialize the client
|
||||||
|
// Go to one of these pages to find your implementation:
|
||||||
|
// postgreSQL: https://orm.drizzle.team/docs/get-started-postgresql
|
||||||
|
// MySQL: https://orm.drizzle.team/docs/get-started-mysql
|
||||||
|
// SQLite: https://orm.drizzle.team/docs/get-started-sqlite
|
||||||
|
// The following is an example for supabase:
|
||||||
|
|
||||||
|
const client = postgres({
|
||||||
|
host: process.env.DB_HOST,
|
||||||
|
user: process.env.DB_USER,
|
||||||
|
password: process.env.DB_PASSWORD,
|
||||||
|
database: process.env.DB_NAME
|
||||||
|
});
|
||||||
|
|
||||||
|
export const db = drizzle(client);
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './schema';
|
||||||
|
export * from './db.server'
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { date, pgTable, serial, text, uuid, varchar } from 'drizzle-orm/pg-core';
|
||||||
|
import { drizzle } from 'drizzle-orm/node-postgres';
|
||||||
|
|
||||||
|
export const usersTable = pgTable('users', {
|
||||||
|
id: uuid('id').primaryKey().defaultRandom(),
|
||||||
|
name: text('name').notNull(),
|
||||||
|
email: text('email').unique().notNull(),
|
||||||
|
hashed_password: text('hashed_password').notNull(),
|
||||||
|
date_created: date('date_created').notNull().defaultNow()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const podsTable = pgTable('pods', {
|
||||||
|
id: uuid('id').primaryKey().defaultRandom(),
|
||||||
|
user_id: uuid('user_id').references(() => usersTable.id),
|
||||||
|
script: text('script').notNull(),
|
||||||
|
date_created: date('date_created').notNull().defaultNow()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const sessionsTable = pgTable('sessions', {
|
||||||
|
id: uuid('id').primaryKey().defaultRandom(),
|
||||||
|
user_id: uuid('user_id').notNull(),
|
||||||
|
date_created: date('date_created').notNull().defaultNow()
|
||||||
|
})
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { db } from "$lib/db/db.server";
|
||||||
|
import { sessionsTable, usersTable } from "$lib/db/schema";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import type { PgUUID } from "drizzle-orm/pg-core";
|
||||||
|
|
||||||
|
export async function createSession(user_id: string) {
|
||||||
|
const session = await db.insert(sessionsTable).values({
|
||||||
|
user_id: user_id
|
||||||
|
}).returning({ id: sessionsTable.id });
|
||||||
|
|
||||||
|
return session[0].id;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function validateSession(session_id: string) {
|
||||||
|
const session = await db.select().from(sessionsTable).where(eq(sessionsTable.id, session_id)).leftJoin(usersTable, eq(usersTable.id, sessionsTable.user_id));
|
||||||
|
|
||||||
|
return session[0].users;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export function generateId(length: number): string {
|
||||||
|
var text = "";
|
||||||
|
var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||||
|
for (var i = 0; i < length; i++)
|
||||||
|
text += possible.charAt(Math.floor(Math.random() * possible.length));
|
||||||
|
return text;
|
||||||
|
}
|
||||||
@@ -2,6 +2,6 @@
|
|||||||
import '../app.pcss';
|
import '../app.pcss';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="max-w-xl mx-auto p-8 h-screen">
|
<div class="max-w-xl mx-auto p-8 h-screen relative">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+33
-10
@@ -2,13 +2,36 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import { Label } from '$lib/components/ui/label';
|
import { Label } from '$lib/components/ui/label';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
|
import Player from '$lib/components/organisms/player.svelte';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { PlayIcon, Loader } from 'lucide-svelte';
|
||||||
|
|
||||||
|
let script = "";
|
||||||
|
|
||||||
|
let opened = false;
|
||||||
|
let pods = [];
|
||||||
|
onMount(async () => {
|
||||||
|
let res = await (await fetch("/")).json();
|
||||||
|
console.log(res);
|
||||||
|
script = res.script;
|
||||||
|
|
||||||
|
res = await (await fetch("/api/pods")).json();
|
||||||
|
console.log(res);
|
||||||
|
pods = res.pods;
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
{#if opened}
|
||||||
|
<Player script={script} on:close={() => opened=false} />
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label>Your Latest Podcast</Label>
|
<Label>Your Latest Podcast</Label>
|
||||||
<Button class="flex w-full flex-row justify-start rounded text-left" variant="secondary">
|
<Button class="flex w-full flex-row justify-start rounded text-left" variant="secondary" on:click={() => opened = true}>
|
||||||
<div class="items-center justify-center rounded-lg p-2">
|
<div class="items-center justify-center rounded-lg p-2 pl-0">
|
||||||
<img src="/icons/play.svg" width={16} height={16} alt="" />
|
<PlayIcon class="h-4 w-4" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-md">Your Daily Pod</h3>
|
<h3 class="text-md">Your Daily Pod</h3>
|
||||||
@@ -16,15 +39,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
<Label>Your Newsletters</Label>
|
<Label>Your Newsletters</Label>
|
||||||
<div class="overflow-scroll">
|
<div class="overflow-x-scroll scrollbars-hidden">
|
||||||
{#each [5, 4, 3, 2, 1] as _}
|
{#each pods as pod, i}
|
||||||
<Button class="flex w-full flex-row justify-start rounded text-left mt-4" variant="secondary">
|
<Button class="mt-4 flex w-full flex-row justify-start rounded text-left" variant="secondary">
|
||||||
<div class="items-center justify-center rounded-lg p-2">
|
<div class="items-center justify-center rounded-lg p-2 pl-0">
|
||||||
<img src="/icons/play.svg" width={16} height={16} alt="" />
|
<PlayIcon class="h-4 w-4" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-md">Your Daily Pod</h3>
|
<h3 class="text-md">{pod.title}</h3>
|
||||||
<p class="text-sm text-muted-foreground">January 18th, 2024</p>
|
<p class="text-sm text-muted-foreground">{new Date(pod.date).toLocaleDateString('de')}</p>
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
/** @type {import('./$types').RequestHandler} */
|
||||||
|
export function GET() {
|
||||||
|
const script = `Opening:
|
||||||
|
"Welcome to VocalCast, the podcast that takes you inside people's inboxes for a glimpse into their digital lives. I'm your host, Alex.
|
||||||
|
|
||||||
|
Today we're browsing through the inbox of Jamie, a 35-year-old marketing manager for a tech startup. Let's dive in!"
|
||||||
|
|
||||||
|
Inbox Overview:
|
||||||
|
"Jamie's inbox currently holds 237 unread emails out of a total of 8,469 messages. The oldest unread email is from 6 weeks ago. Some of the top senders are their boss, Karen, various PR contacts, and the housing community newsletter."
|
||||||
|
|
||||||
|
Email Sampling:
|
||||||
|
|
||||||
|
"Let's take a look at a few emails...Here's one from Jamie's boss asking for an update on the new product launch marketing plan...Another is a pitch from a tech blogger wanting an exclusive interview...Oh and there's the weekly Costco ad!"
|
||||||
|
|
||||||
|
Inbox Insights:
|
||||||
|
"It's clear Jamie gets bombarded with a mix of work and personal emails every day. Based on the number of unread messages piling up, they're likely struggling to keep up and stay on top of it all. The life of a busy professional!"
|
||||||
|
|
||||||
|
Closing:
|
||||||
|
|
||||||
|
"That's all for this glimpse into Jamie's inbox. Join me next week on VocalCast when we go email spelunking into someone else's digital life. I'm your host Alex, thanks for listening!"`;
|
||||||
|
|
||||||
|
return new Response(JSON.stringify({ success: true, script }));
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
export async function GET() {
|
||||||
|
return new Response(JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
pods: [
|
||||||
|
{
|
||||||
|
title: "Your daily pod",
|
||||||
|
date: new Date("2024-03-11")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Your rewind pod",
|
||||||
|
date: new Date("2024-03-10")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Your pod today",
|
||||||
|
date: new Date("2024-03-09")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "My daily pod",
|
||||||
|
date: new Date("2024-03-08")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "I'm just making up stuff at this point",
|
||||||
|
date: new Date("2024-03-07")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Tried that last one to see how long it can be",
|
||||||
|
date: new Date("2024-03-06")
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}))
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { setError, superValidate } from "sveltekit-superforms";
|
||||||
|
import { fail } from "@sveltejs/kit";
|
||||||
|
import { formSchema } from "$lib/components/organisms/auth/schema";
|
||||||
|
import { zod } from "sveltekit-superforms/adapters";
|
||||||
|
import { db, usersTable } from "$lib/db";
|
||||||
|
import bcrypt from "bcrypt";
|
||||||
|
import * as authService from "$lib/services/auth.server";
|
||||||
|
/**
|
||||||
|
* @type {import("./$types").PageServerLoad}
|
||||||
|
*/
|
||||||
|
export const load = async () => {
|
||||||
|
return {
|
||||||
|
form: await superValidate(zod(formSchema)),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {import("@sveltejs/kit").Actions}
|
||||||
|
*/
|
||||||
|
export const actions = {
|
||||||
|
signup: async (event) => {
|
||||||
|
const form = await superValidate(event, zod(formSchema));
|
||||||
|
if (!form.valid) {
|
||||||
|
return fail(400, {
|
||||||
|
form,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// await (async () => {
|
||||||
|
// return new Promise((res, rej) => {
|
||||||
|
// setTimeout(res, 5000)
|
||||||
|
// })
|
||||||
|
// })()
|
||||||
|
|
||||||
|
const newUser = await db.insert(usersTable).values({
|
||||||
|
name: form.data.name,
|
||||||
|
email: form.data.email,
|
||||||
|
hashed_password: (await bcrypt.hash(form.data.password, 10)),
|
||||||
|
}).returning({ id: usersTable.id }).onConflictDoNothing({ target: usersTable.email });
|
||||||
|
|
||||||
|
if (newUser.length === 0) return setError(form, "email", "Email already taken.", {
|
||||||
|
status: 409
|
||||||
|
});
|
||||||
|
|
||||||
|
const sessionId = await authService.createSession(newUser[0].id);
|
||||||
|
|
||||||
|
event.cookies.set("token", sessionId, {
|
||||||
|
path: "/",
|
||||||
|
expires: new Date("01-01-2025"),
|
||||||
|
secure: false
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
form,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,16 +1,27 @@
|
|||||||
<script>
|
<script>
|
||||||
import { Button } from "$lib/components/ui/button";
|
import AuthForm from '$lib/components/organisms/auth/auth-form.svelte';
|
||||||
|
import { Button } from '$lib/components/ui/button';
|
||||||
|
|
||||||
|
let authIsOpen = false;
|
||||||
|
|
||||||
|
/** @type {import('./$types').PageData} */
|
||||||
|
export let data;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="text-center h-full">
|
<div class="h-full text-center">
|
||||||
<img src="/icons/logo.svg" class="mx-auto max-w-xs w-full" alt="">
|
<img src="/icons/logo.svg" class="mx-auto w-full max-w-xs" alt="" />
|
||||||
<h2 class="font-epica text-3xl mt-16">Welcome to VocalCast</h2>
|
<h2 class="font-epica mt-16 text-3xl">Welcome to VocalCast</h2>
|
||||||
|
|
||||||
<p>Turn all your favorite newsletters <br/>into a daily podcast.</p>
|
<p>Turn all your favorite newsletters <br />into a daily podcast.</p>
|
||||||
|
|
||||||
<div class="mt-[100%]">
|
{#if authIsOpen}
|
||||||
<Button class="w-full">Sign up</Button>
|
<div class="pt-8 text-left">
|
||||||
<Button class="w-full mt-4" variant="ghost">Log in</Button>
|
<AuthForm data={data.form} />
|
||||||
</div>
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="mt-[100%]">
|
||||||
|
<Button class="w-full" on:click={() => (authIsOpen = true)}>Sign up</Button>
|
||||||
|
<Button class="mt-4 w-full" variant="ghost">Log in</Button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
Reference in New Issue
Block a user