This commit is contained in:
Omer Sabic 2024-03-13 16:23:39 +01:00
parent d62cc6f6a4
commit aa391a8dba
14 changed files with 222 additions and 111 deletions

View File

@ -4,69 +4,46 @@
@layer base { @layer base {
:root { :root {
--background: rgb(253, 249, 246); --background: 0 0% 100%;
--foreground: 25 67% 4%; --foreground: 20 14.3% 4.1%;
--muted: 25 30% 95%; --card: 0 0% 100%;
--muted-foreground: 25 2% 29%; --card-foreground: 20 14.3% 4.1%;
--popover: 25 70% 98%; --popover: 0 0% 100%;
--popover-foreground: 25 67% 4%; --popover-foreground: 20 14.3% 4.1%;
--card: 25 70% 98%; --primary: 24.6 95% 53.1%;
--card-foreground: 25 67% 4%; --primary-foreground: 60 9.1% 97.8%;
--border: 220 13% 91%; --secondary: 60 4.8% 95.9%;
--input: 220 13% 91%; --secondary-foreground: 24 9.8% 10%;
--primary: 25 31% 75%; --muted: 60 4.8% 95.9%;
--primary-foreground: 25 31% 15%; --muted-foreground: 25 5.3% 44.7%;
--secondary: 25 18% 90%; --accent: 60 4.8% 95.9%;
--secondary-foreground: 25 18% 30%; --accent-foreground: 24 9.8% 10%;
--accent: 25 23% 83%; --destructive: 0 72.22% 50.59%;
--accent-foreground: 25 23% 23%; --destructive-foreground: 60 9.1% 97.8%;
--destructive: 13 96% 20%; --border: 20 5.9% 90%;
--destructive-foreground: 13 96% 80%; --input: 20 5.9% 90%;
--ring: 25 31% 75%; --ring: 24.6 95% 53.1%;
--radius: 0.5rem; --radius: 0.5rem;
font-size: 16px;
} }
.dark { .dark {
--background: 25 41% 2%; --background: 20 14.3% 4.1%;
--foreground: 25 21% 98%; --foreground: 60 9.1% 97.8%;
--muted: 25 30% 5%; --card: 20 14.3% 4.1%;
--muted-foreground: 25 2% 71%; --card-foreground: 60 9.1% 97.8%;
--popover: 25 41% 2%; --popover: 20 14.3% 4.1%;
--popover-foreground: 25 21% 98%; --popover-foreground: 60 9.1% 97.8%;
--card: 25 41% 2%; --primary: 20.5 90.2% 48.2%;
--card-foreground: 25 21% 98%; --primary-foreground: 60 9.1% 97.8%;
--border: 215 27.9% 16.9%; --secondary: 12 6.5% 15.1%;
--input: 215 27.9% 16.9%; --secondary-foreground: 60 9.1% 97.8%;
--primary: 25 31% 75%; --muted: 12 6.5% 15.1%;
--primary-foreground: 25 31% 15%; --muted-foreground: 24 5.4% 63.9%;
--secondary: 25 5% 14%; --accent: 12 6.5% 15.1%;
--secondary-foreground: 25 5% 74%; --accent-foreground: 60 9.1% 97.8%;
--accent: 25 11% 20%; --destructive: 0 72.2% 50.6%;
--accent-foreground: 25 11% 80%; --destructive-foreground: 60 9.1% 97.8%;
--destructive: 13 96% 49%; --border: 12 6.5% 15.1%;
--destructive-foreground: 0 0% 100%; --input: 12 6.5% 15.1%;
--ring: 25 31% 75%; --ring: 20.5 90.2% 48.2%;
} }
}
@font-face {
font-family: 'Epica';
font-style: normal;
font-weight: 500;
src: url('/fonts/epica.otf');
}
.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+ */
} }

View File

@ -9,6 +9,7 @@
superForm, superForm,
} from "sveltekit-superforms"; } from "sveltekit-superforms";
import { zodClient } from "sveltekit-superforms/adapters"; import { zodClient } from "sveltekit-superforms/adapters";
import { redirect } from "@sveltejs/kit";
let isLoading = false; let isLoading = false;
@ -21,6 +22,7 @@
}, },
onUpdated: ({form: f}) => { onUpdated: ({form: f}) => {
isLoading = false; isLoading = false;
if(!f.errors) redirect(303, "/");
} }
}); });

View File

@ -9,6 +9,7 @@
superForm, superForm,
} from "sveltekit-superforms"; } from "sveltekit-superforms";
import { zodClient } from "sveltekit-superforms/adapters"; import { zodClient } from "sveltekit-superforms/adapters";
import { redirect } from "@sveltejs/kit";
let isLoading = false; let isLoading = false;
@ -21,6 +22,7 @@
}, },
onUpdated: ({form: f}) => { onUpdated: ({form: f}) => {
isLoading = false; isLoading = false;
if(!f.errors) redirect(303, "/")
} }
}); });

View File

@ -4,7 +4,9 @@
import * as Drawer from '../ui/drawer'; import * as Drawer from '../ui/drawer';
import { Input } from '../ui/input'; import { Input } from '../ui/input';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import { createEventDispatcher } from 'svelte';
const dispatcher = createEventDispatcher();
let group_name = ''; let group_name = '';
async function createGroup() { async function createGroup() {
@ -20,11 +22,12 @@
).json(); ).json();
if (!groups.success) return toast.error('Problem with creating group.'); if (!groups.success) return toast.error('Problem with creating group.');
dispatcher("created")
return toast.success('Group created.'); return toast.success('Group created.');
} }
</script> </script>
<Drawer.Root> <Drawer.Root open={false}>
<Drawer.Trigger asChild let:builder> <Drawer.Trigger asChild let:builder>
<Button builders={[builder]} class="w-full rounded" variant="outline"> <Button builders={[builder]} class="w-full rounded" variant="outline">
<div class="flex flex-row justify-center items-center"> <div class="flex flex-row justify-center items-center">

View File

@ -1,18 +1,19 @@
<script> <script>
import { browser } from '$app/environment'; import { browser } from '$app/environment';
import { ArrowLeft, PauseIcon, Play } from 'lucide-svelte'; import { ArrowLeft, PauseIcon, Play } from 'lucide-svelte';
import { createEventDispatcher, onMount } from 'svelte'; import { createEventDispatcher, onDestroy, onMount } from 'svelte';
import {Progress} from '$lib/components/ui/progress'; import { Progress } from '$lib/components/ui/progress';
/** @type {string} */ import { isOpen, script, paused } from './state';
export let script; import { Button } from '$lib/components/ui/button';
/** @type {SpeechSynthesisUtterance} */ /** @type {SpeechSynthesisUtterance} */
let utterance; let utterance;
let paused = false;
let progress = 0; let progress = 0;
/** @type {number} */ /** @type {number} */
let wordIndex = 0; let wordIndex = 0;
let text = $script;
/** /**
* @type {SpeechSynthesis} * @type {SpeechSynthesis}
*/ */
@ -23,13 +24,13 @@
function ready() { function ready() {
speechSynthesis.cancel(); speechSynthesis.cancel();
const text = script; const text = $script;
const words = text.split(/\s+/); const words = text.split(/\s+/);
utterance = new SpeechSynthesisUtterance(text); utterance = new SpeechSynthesisUtterance(text);
utterance.addEventListener('boundary', (event) => { utterance.addEventListener('boundary', (event) => {
if (event.name === 'word') { if (event.name === 'word') {
console.log(words[wordIndex]); console.log(words[wordIndex]);
document.querySelector(`span[data-index="${wordIndex}"]`)?.classList.add('text-black'); document.querySelector(`span[data-index="${wordIndex}"]`)?.classList.add('text-primary');
document.querySelector(`span[data-index="${wordIndex}"]`)?.scrollIntoView({ document.querySelector(`span[data-index="${wordIndex}"]`)?.scrollIntoView({
behavior: 'smooth', behavior: 'smooth',
block: 'center', block: 'center',
@ -42,7 +43,7 @@
}); });
utterance.addEventListener('start', (event) => { utterance.addEventListener('start', (event) => {
paused = false; paused.set(false);
// const length = words.length; // const length = words.length;
// const updateProgress = () => { // const updateProgress = () => {
@ -54,13 +55,15 @@
// updateProgress(); // updateProgress();
}); });
utterance.addEventListener('pause', () => { utterance.onpause = () => {
paused = true; paused.set(true);
}); console.log('pause');
};
utterance.addEventListener('resume', () => { utterance.onresume = () => {
paused = false; paused.set(false);
}); console.log('resume');
};
utterance.addEventListener('end', () => { utterance.addEventListener('end', () => {
progress = 0; progress = 0;
@ -71,23 +74,32 @@
const pauseOrResumeUtterance = () => { const pauseOrResumeUtterance = () => {
if (!speechSynthesis) return; if (!speechSynthesis) return;
console.log('pause or resume');
if (!speechSynthesis.speaking) { if (!speechSynthesis.speaking) {
ready(); ready();
return; return;
} }
if (paused) { if ($paused) {
console.log('resume');
speechSynthesis.resume(); speechSynthesis.resume();
paused.set(false);
} else { } else {
console.log('pause');
speechSynthesis.pause(); speechSynthesis.pause();
paused.set(true);
} }
}; };
onMount(async () => { onMount(async () => {
if (browser && 'speechSynthesis' in window) { if (browser && 'speechSynthesis' in window) {
speechSynthesis = window.speechSynthesis; speechSynthesis = window.speechSynthesis;
const unsubscribe = script.subscribe(v => {
text = v;
ready();
document.querySelectorAll(`span[data-index].text-primary`).forEach(e=>e.classList.remove('text-primary'));
wordIndex = 0;
progress = 0;
// speechSynthesis.pause();
// paused.set(true);
});
// onDestroy(unsubscribe);
} else { } else {
alert( 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>.' '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>.'
@ -96,32 +108,35 @@
}); });
</script> </script>
<div class="absolute inset-0 flex h-full w-full bg-white"> <div class="absolute inset-0 flex flex-grow h-full w-full max-w-[100vw] bg-background {!$isOpen ? "hidden" : ""} z-50">
<div <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" class="flex flex-col flex-grow gap-16 p-10 max-w-[100vw]"
> >
<button on:click={() => dispatcher('close')}> <button on:click={() => {
dispatcher('close')
isOpen.set(false);
}}>
<ArrowLeft class="h-8 w-8" /> <ArrowLeft class="h-8 w-8" />
</button> </button>
<div <div
class="text-container script scrollbars-hidden h-[80%] overflow-scroll text-2xl pb-8" class="text-container script scrollbars-hidden h-[80%] w-full overflow-y-scroll break-words text-wrap pb-8 text-2xl"
style="line-height: 3rem; color:rgba(0,0,0,0.5)" style="line-height: 3rem;"
> >
{#each script.split(/\s+/) as word, i} {#each text.split(/\s+/) as word, i}
<span data-index={i}>{word} </span> <span data-index={i}>{word} </span>
{/each} {/each}
</div> </div>
<Progress class="w-full" max={script.length} value={progress} /> <Progress class="w-full" max={text.length} value={progress} />
<button <Button
class="m-0 mx-auto flex aspect-square w-12 items-center justify-center rounded-full bg-white p-0" class="m-0 mx-auto flex aspect-square w-12 items-center justify-center rounded-full p-0"
on:click={pauseOrResumeUtterance} on:click={pauseOrResumeUtterance}
> >
{#if paused} {#if $paused}
<Play class="h-6 w-6" /> <Play class="h-6 w-6" />
{:else} {:else}
<PauseIcon class="h-6 w-6" /> <PauseIcon class="h-6 w-6" />
{/if} {/if}
</button> </Button>
</div> </div>
</div> </div>

View File

@ -0,0 +1,5 @@
import { writable } from "svelte/store";
export const isOpen = writable(false);
export const script = writable("Hello world");
export const paused = writable(true);

View File

@ -3,15 +3,19 @@
import { Label } from "$lib/components/ui/label"; import { Label } from "$lib/components/ui/label";
import { Textarea } from "$lib/components/ui/textarea" import { Textarea } from "$lib/components/ui/textarea"
import { toast } from "svelte-sonner";
export let text; export let text;
export let title = "Your relay email";
</script> </script>
<div> <div>
<Label for="email">Your relay email</Label> <Label for="email">{title}</Label>
<Textarea name="email" value={text} class="min-h-[1rem] h-[2.75rem] text-[1rem] text-center resize-none" readonly on:click={(e) => { <Textarea name="email" value={text} class="min-h-[1rem] h-[2.75rem] text-[1rem] text-center resize-none" readonly on:click={(e) => {
e.target.select(); e.target.select();
navigator.clipboard.writeText(e.target.value); navigator.clipboard.writeText(e.target.value);
e.target.unselect() toast("Email coppied");
e.target.unselect();
}}/> }}/>
<p class="text-sm text-muted-foreground">Subscribe to newsletters with this email in order to add them to your group.</p>
</div> </div>

View File

@ -13,7 +13,7 @@
<DrawerOverlay /> <DrawerOverlay />
<DrawerPrimitive.Content <DrawerPrimitive.Content
class={cn( class={cn(
"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border border-black bg-secondary", "fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border border-black bg-background",
className className
)} )}
{...$$restProps} {...$$restProps}

View File

@ -1,10 +1,9 @@
import bcrypt from "bcrypt"; import bcrypt from "bcrypt";
export function generateId(length: number): string { export function generateId(length: number, charset: string = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"): string {
var text = ""; var text = "";
var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for (var i = 0; i < length; i++) for (var i = 0; i < length; i++)
text += possible.charAt(Math.floor(Math.random() * possible.length)); text += charset.charAt(Math.floor(Math.random() * charset.length));
return text; return text;
} }

View File

@ -1,10 +1,13 @@
<script> <script>
import '../app.pcss'; import '../app.pcss';
import { Toaster } from '$lib/components/ui/sonner'; import { Toaster } from '$lib/components/ui/sonner';
import Player from '$lib/components/organisms/player/player.svelte';
</script> </script>
<Toaster />
<div class="max-w-xl mx-auto p-8 h-screen relative"> <div class="max-w-xl mx-auto p-8 h-screen relative">
<Player />
<Toaster />
<slot /> <slot />
</div> </div>

View File

@ -2,14 +2,13 @@
// @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 Player from '$lib/components/organisms/player/player.svelte';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { PlayIcon, Loader, Plus, MailIcon, PlusIcon } from 'lucide-svelte'; import { PlayIcon, Loader, Plus, MailIcon, PlusIcon } from 'lucide-svelte';
import CreateGroup from '$lib/components/organisms/create-group.svelte'; import CreateGroup from '$lib/components/organisms/create-group.svelte';
import { isOpen, script } from '$lib/components/organisms/player/state';
let script = "";
let opened = false;
let groups = []; let groups = [];
let pods = [] let pods = []
onMount(async () => { onMount(async () => {
@ -21,16 +20,17 @@
groups = res.groups; groups = res.groups;
}); });
</script>
{#if opened} $: groups;
<Player script={script} on:close={() => opened=false} /> </script>
{/if}
<div class="flex gap-4 flex-col"> <div class="flex gap-4 flex-col">
<Label>Your Latest Podcasts</Label> <Label>Your Latest Podcasts</Label>
{#each pods as pod} {#each pods as pod}
<Button class="flex w-full flex-row justify-start rounded text-left" variant="secondary" on:click={() => opened = true}> <Button class="flex w-full flex-row justify-start rounded text-left" variant="secondary" on:click={() => {
isOpen.set(true);
script.set(pod.script);
}}>
<div class="items-center justify-center rounded-lg p-2 pl-0"> <div class="items-center justify-center rounded-lg p-2 pl-0">
<PlayIcon class="h-4 w-4" /> <PlayIcon class="h-4 w-4" />
</div> </div>
@ -42,10 +42,10 @@
{/each} {/each}
<Label>Your Groups</Label> <Label>Your Groups</Label>
<div class="overflow-x-scroll scrollbars-hidden"> <div class="scrollbars-hidden">
<CreateGroup /> <CreateGroup on:created={() => groups = [...groups]} />
{#each groups as group} {#each groups as group}
<Button class="mt-4 flex w-full flex-row justify-start rounded text-left" variant="secondary"> <Button class="mt-4 flex w-full flex-row justify-start rounded text-left" variant="secondary" href="/group/{group.id}">
<div class="items-center justify-center rounded-lg p-2 pl-0"> <div class="items-center justify-center rounded-lg p-2 pl-0">
<MailIcon class="h-4 w-4" /> <MailIcon class="h-4 w-4" />
</div> </div>

View File

@ -7,7 +7,6 @@ import { eq } from "drizzle-orm";
* @type {import("@sveltejs/kit").RequestHandler} * @type {import("@sveltejs/kit").RequestHandler}
*/ */
export async function GET(event) { export async function GET(event) {
console.log(event.locals.user.id)
const groups = await db.select().from(groupsTable).where(eq(groupsTable.userId, event.locals.user.id)); const groups = await db.select().from(groupsTable).where(eq(groupsTable.userId, event.locals.user.id));
return new Response(JSON.stringify({ return new Response(JSON.stringify({
@ -43,4 +42,21 @@ export async function POST(event) {
success: true, success: true,
group: groups[0] group: groups[0]
}); });
}
/**
* @type {import("@sveltejs/kit").RequestHandler}
*/
export async function DELETE(event) {
const url = new URL(event.request.url);
const id = url.searchParams.get("id");
if(!id) return json({
succcess: false
});
await db.delete(groupsTable).where(eq(groupsTable.id, id));
return json({
success: true
})
} }

View File

@ -0,0 +1,28 @@
import { db, groupsTable, podsTable, sourcesTable } from "$lib/db";
import { redirect } from "@sveltejs/kit";
import { and, eq } from "drizzle-orm";
/**
* @type {import("./$types").PageServerLoad}
*/
export const load = async (event) => {
let groupInfo;
let sources;
let pods;
try {
groupInfo = await db.select().from(groupsTable).where(and(eq(groupsTable.userId, event.locals.user.id), eq(groupsTable.id, event.params.groupid)));
if(!groupInfo[0]) return redirect(303, "/")
sources = await db.select().from(sourcesTable).where(eq(sourcesTable.groupId, event.params.groupid));
pods = await db.select().from(podsTable).where(eq(podsTable.groupId, event.params.groupid));
} catch (err) {
return redirect(303, "/");
}
return {
group: groupInfo[0],
sources,
pods
};
}

View File

@ -0,0 +1,57 @@
<script>
import RelayEmail from '$lib/components/organisms/relay-email.svelte';
import { Button } from '$lib/components/ui/button';
import { Label } from '$lib/components/ui/label';
import { isOpen, script } from '$lib/components/organisms/player/state';
import { MailIcon, PlayIcon } from 'lucide-svelte';
import { redirect } from '@sveltejs/kit';
/**
* @type {import("./$types").PageServerData}
*/
export let data;
</script>
<div class="flex flex-col gap-4">
<h1 class="text-lg">{data.group.name}</h1>
<Label>Your Latest Podcasts</Label>
<div class="flex overflow-x-scroll gap-3">
{#each data.pods as pod}
<Button
class="flex flex-row justify-start rounded text-left w-[15rem] max-w-sm"
variant="secondary"
on:click={() => {
isOpen.set(true);
script.set(pod.script);
}}
>
<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">
{new Date(pod.date_created).toLocaleDateString('de')}
</p>
</div>
</Button>
{/each}
</div>
<RelayEmail title="This group's email" text={data.group.email} />
<Label>Your Sources</Label>
<div class="scrollbars-hidden flex flex-col gap-4">
{#each data.sources as source}
<Button class="flex w-full flex-row justify-start rounded text-left" variant="secondary">
<div class="items-center justify-center rounded-lg p-2 pl-0">
<MailIcon class="h-4 w-4" />
</div>
<div>
<h3 class="text-md">{source.name}</h3>
</div>
</Button>
{/each}
</div>
</div>