auth + player

This commit is contained in:
Omer Sabic 2024-03-11 15:29:15 +01:00
parent 8b4c542cc3
commit 40d2342211
38 changed files with 6229 additions and 120 deletions

File diff suppressed because it is too large Load Diff

15
drizzle.config.ts Normal file
View 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;

View 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 $$;

View File

@ -0,0 +1 @@
ALTER TABLE "pods" ADD COLUMN "date_created" date DEFAULT now() NOT NULL;

View 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;

View 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": {}
}
}

View 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": {}
}
}

View 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": {}
}
}

View 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

File diff suppressed because it is too large Load Diff

View File

@ -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"
}
}

View File

@ -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
View 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);
}

View 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>

View 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;

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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,
};

View File

@ -0,0 +1,7 @@
import Root from "./progress.svelte";
export {
Root,
//
Root as Progress,
};

View 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
View 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
View File

@ -0,0 +1,2 @@
export * from './schema';
export * from './db.server'

23
src/lib/db/schema.ts Normal file
View 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()
})

View 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;
}

View 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;
}

View File

@ -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>

View File

@ -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
View 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 }));
}

View 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")
}
]
}))
}

View 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,
};
},
};

View File

@ -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>