auth + player
This commit is contained in:
parent
8b4c542cc3
commit
40d2342211
2938
diagrams/basic-flow.excalidraw
Normal file
2938
diagrams/basic-flow.excalidraw
Normal file
File diff suppressed because it is too large
Load Diff
15
drizzle.config.ts
Normal file
15
drizzle.config.ts
Normal file
@ -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;
|
24
drizzle/0000_lonely_moonstone.sql
Normal file
24
drizzle/0000_lonely_moonstone.sql
Normal file
@ -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 $$;
|
1
drizzle/0001_married_nomad.sql
Normal file
1
drizzle/0001_married_nomad.sql
Normal file
@ -0,0 +1 @@
|
||||
ALTER TABLE "pods" ADD COLUMN "date_created" date DEFAULT now() NOT NULL;
|
2
drizzle/0002_zippy_the_hunter.sql
Normal file
2
drizzle/0002_zippy_the_hunter.sql
Normal file
@ -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;
|
124
drizzle/meta/0000_snapshot.json
Normal file
124
drizzle/meta/0000_snapshot.json
Normal file
@ -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": {}
|
||||
}
|
||||
}
|
131
drizzle/meta/0001_snapshot.json
Normal file
131
drizzle/meta/0001_snapshot.json
Normal file
@ -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": {}
|
||||
}
|
||||
}
|
145
drizzle/meta/0002_snapshot.json
Normal file
145
drizzle/meta/0002_snapshot.json
Normal file
@ -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": {}
|
||||
}
|
||||
}
|
27
drizzle/meta/_journal.json
Normal file
27
drizzle/meta/_journal.json
Normal file
@ -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
|
||||
}
|
||||
]
|
||||
}
|
2177
package-lock.json
generated
2177
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
13
package.json
13
package.json
@ -15,7 +15,9 @@
|
||||
"@sveltejs/adapter-auto": "^3.0.0",
|
||||
"@sveltejs/kit": "^2.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
||||
"@types/bcrypt": "^5.0.2",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"drizzle-kit": "^0.20.14",
|
||||
"postcss": "^8.4.32",
|
||||
"postcss-load-config": "^5.0.2",
|
||||
"prettier": "^3.1.1",
|
||||
@ -30,10 +32,17 @@
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"bits-ui": "^0.19.5",
|
||||
"bcrypt": "^5.1.1",
|
||||
"bits-ui": "^0.19.6",
|
||||
"clsx": "^2.1.0",
|
||||
"dotenv": "^16.4.5",
|
||||
"drizzle-orm": "^0.30.1",
|
||||
"formsnap": "^0.5.1",
|
||||
"lucide-svelte": "^0.354.0",
|
||||
"pg": "^8.11.3",
|
||||
"sveltekit-superforms": "^2.8.1",
|
||||
"tailwind-merge": "^2.2.1",
|
||||
"tailwind-variants": "^0.2.0"
|
||||
"tailwind-variants": "^0.2.0",
|
||||
"zod": "^3.22.4"
|
||||
}
|
||||
}
|
||||
|
12
src/app.pcss
12
src/app.pcss
@ -4,7 +4,7 @@
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 25 70% 98%;
|
||||
--background: rgb(253, 249, 246);
|
||||
--foreground: 25 67% 4%;
|
||||
--muted: 25 30% 95%;
|
||||
--muted-foreground: 25 2% 29%;
|
||||
@ -59,4 +59,14 @@
|
||||
|
||||
.font-epica {
|
||||
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+ */
|
||||
}
|
17
src/hooks.server.js
Normal file
17
src/hooks.server.js
Normal file
@ -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);
|
||||
}
|
80
src/lib/components/organisms/auth/auth-form.svelte
Normal file
80
src/lib/components/organisms/auth/auth-form.svelte
Normal file
@ -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>
|
||||
|
9
src/lib/components/organisms/auth/schema.ts
Normal file
9
src/lib/components/organisms/auth/schema.ts
Normal file
@ -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;
|
142
src/lib/components/organisms/player.svelte
Normal file
142
src/lib/components/organisms/player.svelte
Normal file
@ -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>
|
10
src/lib/components/ui/form/form-button.svelte
Normal file
10
src/lib/components/ui/form/form-button.svelte
Normal file
@ -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>
|
17
src/lib/components/ui/form/form-description.svelte
Normal file
17
src/lib/components/ui/form/form-description.svelte
Normal file
@ -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>
|
26
src/lib/components/ui/form/form-element-field.svelte
Normal file
26
src/lib/components/ui/form/form-element-field.svelte
Normal file
@ -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>
|
26
src/lib/components/ui/form/form-field-errors.svelte
Normal file
26
src/lib/components/ui/form/form-field-errors.svelte
Normal file
@ -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>
|
26
src/lib/components/ui/form/form-field.svelte
Normal file
26
src/lib/components/ui/form/form-field.svelte
Normal file
@ -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>
|
31
src/lib/components/ui/form/form-fieldset.svelte
Normal file
31
src/lib/components/ui/form/form-fieldset.svelte
Normal file
@ -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>
|
17
src/lib/components/ui/form/form-label.svelte
Normal file
17
src/lib/components/ui/form/form-label.svelte
Normal file
@ -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>
|
17
src/lib/components/ui/form/form-legend.svelte
Normal file
17
src/lib/components/ui/form/form-legend.svelte
Normal file
@ -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>
|
33
src/lib/components/ui/form/index.ts
Normal file
33
src/lib/components/ui/form/index.ts
Normal file
@ -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,
|
||||
};
|
7
src/lib/components/ui/progress/index.ts
Normal file
7
src/lib/components/ui/progress/index.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import Root from "./progress.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Progress,
|
||||
};
|
21
src/lib/components/ui/progress/progress.svelte
Normal file
21
src/lib/components/ui/progress/progress.svelte
Normal file
@ -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>
|
20
src/lib/db/db.server.ts
Normal file
20
src/lib/db/db.server.ts
Normal file
@ -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);
|
2
src/lib/db/index.ts
Normal file
2
src/lib/db/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './schema';
|
||||
export * from './db.server'
|
23
src/lib/db/schema.ts
Normal file
23
src/lib/db/schema.ts
Normal file
@ -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()
|
||||
})
|
18
src/lib/services/auth.server.ts
Normal file
18
src/lib/services/auth.server.ts
Normal file
@ -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;
|
||||
}
|
7
src/lib/utils/auth.utils.ts
Normal file
7
src/lib/utils/auth.utils.ts
Normal file
@ -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';
|
||||
</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 />
|
||||
</div>
|
||||
|
@ -2,13 +2,36 @@
|
||||
// @ts-nocheck
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
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>
|
||||
|
||||
{#if opened}
|
||||
<Player script={script} on:close={() => opened=false} />
|
||||
{/if}
|
||||
|
||||
<div>
|
||||
<Label>Your Latest Podcast</Label>
|
||||
<Button class="flex w-full flex-row justify-start rounded text-left" variant="secondary">
|
||||
<div class="items-center justify-center rounded-lg p-2">
|
||||
<img src="/icons/play.svg" width={16} height={16} alt="" />
|
||||
<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 pl-0">
|
||||
<PlayIcon class="h-4 w-4" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-md">Your Daily Pod</h3>
|
||||
@ -16,17 +39,17 @@
|
||||
</div>
|
||||
</Button>
|
||||
<Label>Your Newsletters</Label>
|
||||
<div class="overflow-scroll">
|
||||
{#each [5, 4, 3, 2, 1] as _}
|
||||
<Button class="flex w-full flex-row justify-start rounded text-left mt-4" variant="secondary">
|
||||
<div class="items-center justify-center rounded-lg p-2">
|
||||
<img src="/icons/play.svg" width={16} height={16} alt="" />
|
||||
<div class="overflow-x-scroll scrollbars-hidden">
|
||||
{#each pods as pod, i}
|
||||
<Button class="mt-4 flex w-full flex-row justify-start rounded text-left" variant="secondary">
|
||||
<div class="items-center justify-center rounded-lg p-2 pl-0">
|
||||
<PlayIcon class="h-4 w-4" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-md">Your Daily Pod</h3>
|
||||
<p class="text-sm text-muted-foreground">January 18th, 2024</p>
|
||||
<h3 class="text-md">{pod.title}</h3>
|
||||
<p class="text-sm text-muted-foreground">{new Date(pod.date).toLocaleDateString('de')}</p>
|
||||
</div>
|
||||
</Button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
23
src/routes/+server.js
Normal file
23
src/routes/+server.js
Normal file
@ -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 }));
|
||||
}
|
31
src/routes/api/pods/+server.js
Normal file
31
src/routes/api/pods/+server.js
Normal file
@ -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")
|
||||
}
|
||||
]
|
||||
}))
|
||||
}
|
57
src/routes/auth/+page.server.js
Normal file
57
src/routes/auth/+page.server.js
Normal file
@ -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>
|
||||
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>
|
||||
|
||||
<div class="text-center h-full">
|
||||
<img src="/icons/logo.svg" class="mx-auto max-w-xs w-full" alt="">
|
||||
<h2 class="font-epica text-3xl mt-16">Welcome to VocalCast</h2>
|
||||
|
||||
<p>Turn all your favorite newsletters <br/>into a daily podcast.</p>
|
||||
<div class="h-full text-center">
|
||||
<img src="/icons/logo.svg" class="mx-auto w-full max-w-xs" alt="" />
|
||||
<h2 class="font-epica mt-16 text-3xl">Welcome to VocalCast</h2>
|
||||
|
||||
<div class="mt-[100%]">
|
||||
<Button class="w-full">Sign up</Button>
|
||||
<Button class="w-full mt-4" variant="ghost">Log in</Button>
|
||||
</div>
|
||||
</div>
|
||||
<p>Turn all your favorite newsletters <br />into a daily podcast.</p>
|
||||
|
||||
{#if authIsOpen}
|
||||
<div class="pt-8 text-left">
|
||||
<AuthForm data={data.form} />
|
||||
</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>
|
||||
|
Loading…
Reference in New Issue
Block a user