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 {
:root {
--background: rgb(253, 249, 246);
--foreground: 25 67% 4%;
--muted: 25 30% 95%;
--muted-foreground: 25 2% 29%;
--popover: 25 70% 98%;
--popover-foreground: 25 67% 4%;
--card: 25 70% 98%;
--card-foreground: 25 67% 4%;
--border: 220 13% 91%;
--input: 220 13% 91%;
--primary: 25 31% 75%;
--primary-foreground: 25 31% 15%;
--secondary: 25 18% 90%;
--secondary-foreground: 25 18% 30%;
--accent: 25 23% 83%;
--accent-foreground: 25 23% 23%;
--destructive: 13 96% 20%;
--destructive-foreground: 13 96% 80%;
--ring: 25 31% 75%;
--background: 0 0% 100%;
--foreground: 20 14.3% 4.1%;
--card: 0 0% 100%;
--card-foreground: 20 14.3% 4.1%;
--popover: 0 0% 100%;
--popover-foreground: 20 14.3% 4.1%;
--primary: 24.6 95% 53.1%;
--primary-foreground: 60 9.1% 97.8%;
--secondary: 60 4.8% 95.9%;
--secondary-foreground: 24 9.8% 10%;
--muted: 60 4.8% 95.9%;
--muted-foreground: 25 5.3% 44.7%;
--accent: 60 4.8% 95.9%;
--accent-foreground: 24 9.8% 10%;
--destructive: 0 72.22% 50.59%;
--destructive-foreground: 60 9.1% 97.8%;
--border: 20 5.9% 90%;
--input: 20 5.9% 90%;
--ring: 24.6 95% 53.1%;
--radius: 0.5rem;
font-size: 16px;
}
.dark {
--background: 25 41% 2%;
--foreground: 25 21% 98%;
--muted: 25 30% 5%;
--muted-foreground: 25 2% 71%;
--popover: 25 41% 2%;
--popover-foreground: 25 21% 98%;
--card: 25 41% 2%;
--card-foreground: 25 21% 98%;
--border: 215 27.9% 16.9%;
--input: 215 27.9% 16.9%;
--primary: 25 31% 75%;
--primary-foreground: 25 31% 15%;
--secondary: 25 5% 14%;
--secondary-foreground: 25 5% 74%;
--accent: 25 11% 20%;
--accent-foreground: 25 11% 80%;
--destructive: 13 96% 49%;
--destructive-foreground: 0 0% 100%;
--ring: 25 31% 75%;
--background: 20 14.3% 4.1%;
--foreground: 60 9.1% 97.8%;
--card: 20 14.3% 4.1%;
--card-foreground: 60 9.1% 97.8%;
--popover: 20 14.3% 4.1%;
--popover-foreground: 60 9.1% 97.8%;
--primary: 20.5 90.2% 48.2%;
--primary-foreground: 60 9.1% 97.8%;
--secondary: 12 6.5% 15.1%;
--secondary-foreground: 60 9.1% 97.8%;
--muted: 12 6.5% 15.1%;
--muted-foreground: 24 5.4% 63.9%;
--accent: 12 6.5% 15.1%;
--accent-foreground: 60 9.1% 97.8%;
--destructive: 0 72.2% 50.6%;
--destructive-foreground: 60 9.1% 97.8%;
--border: 12 6.5% 15.1%;
--input: 12 6.5% 15.1%;
--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,
} from "sveltekit-superforms";
import { zodClient } from "sveltekit-superforms/adapters";
import { redirect } from "@sveltejs/kit";
let isLoading = false;
@ -21,6 +22,7 @@
},
onUpdated: ({form: f}) => {
isLoading = false;
if(!f.errors) redirect(303, "/");
}
});

View File

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

View File

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

View File

@ -1,18 +1,19 @@
<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;
import { createEventDispatcher, onDestroy, onMount } from 'svelte';
import { Progress } from '$lib/components/ui/progress';
import { isOpen, script, paused } from './state';
import { Button } from '$lib/components/ui/button';
/** @type {SpeechSynthesisUtterance} */
let utterance;
let paused = false;
let progress = 0;
/** @type {number} */
let wordIndex = 0;
let text = $script;
/**
* @type {SpeechSynthesis}
*/
@ -23,13 +24,13 @@
function ready() {
speechSynthesis.cancel();
const text = script;
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}"]`)?.classList.add('text-primary');
document.querySelector(`span[data-index="${wordIndex}"]`)?.scrollIntoView({
behavior: 'smooth',
block: 'center',
@ -42,7 +43,7 @@
});
utterance.addEventListener('start', (event) => {
paused = false;
paused.set(false);
// const length = words.length;
// const updateProgress = () => {
@ -54,13 +55,15 @@
// updateProgress();
});
utterance.addEventListener('pause', () => {
paused = true;
});
utterance.onpause = () => {
paused.set(true);
console.log('pause');
};
utterance.addEventListener('resume', () => {
paused = false;
});
utterance.onresume = () => {
paused.set(false);
console.log('resume');
};
utterance.addEventListener('end', () => {
progress = 0;
@ -71,23 +74,32 @@
const pauseOrResumeUtterance = () => {
if (!speechSynthesis) return;
console.log('pause or resume');
if (!speechSynthesis.speaking) {
ready();
return;
}
if (paused) {
console.log('resume');
if ($paused) {
speechSynthesis.resume();
paused.set(false);
} else {
console.log('pause');
speechSynthesis.pause();
paused.set(true);
}
};
onMount(async () => {
if (browser && 'speechSynthesis' in window) {
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 {
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>.'
@ -96,32 +108,35 @@
});
</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
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" />
</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)"
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;"
>
{#each script.split(/\s+/) as word, i}
{#each text.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"
<Progress class="w-full" max={text.length} value={progress} />
<Button
class="m-0 mx-auto flex aspect-square w-12 items-center justify-center rounded-full p-0"
on:click={pauseOrResumeUtterance}
>
{#if paused}
{#if $paused}
<Play class="h-6 w-6" />
{:else}
<PauseIcon class="h-6 w-6" />
{/if}
</button>
</Button>
</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 { Textarea } from "$lib/components/ui/textarea"
import { toast } from "svelte-sonner";
export let text;
export let title = "Your relay email";
</script>
<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) => {
e.target.select();
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>

View File

@ -13,7 +13,7 @@
<DrawerOverlay />
<DrawerPrimitive.Content
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
)}
{...$$restProps}

View File

@ -1,10 +1,9 @@
import bcrypt from "bcrypt";
export function generateId(length: number): string {
export function generateId(length: number, charset: string = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"): string {
var text = "";
var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
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;
}

View File

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

View File

@ -2,14 +2,13 @@
// @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 Player from '$lib/components/organisms/player/player.svelte';
import { onMount } from 'svelte';
import { PlayIcon, Loader, Plus, MailIcon, PlusIcon } from 'lucide-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 pods = []
onMount(async () => {
@ -21,16 +20,17 @@
groups = res.groups;
});
</script>
{#if opened}
<Player script={script} on:close={() => opened=false} />
{/if}
$: groups;
</script>
<div class="flex gap-4 flex-col">
<Label>Your Latest Podcasts</Label>
{#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">
<PlayIcon class="h-4 w-4" />
</div>
@ -42,10 +42,10 @@
{/each}
<Label>Your Groups</Label>
<div class="overflow-x-scroll scrollbars-hidden">
<CreateGroup />
<div class="scrollbars-hidden">
<CreateGroup on:created={() => groups = [...groups]} />
{#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">
<MailIcon class="h-4 w-4" />
</div>

View File

@ -7,7 +7,6 @@ import { eq } from "drizzle-orm";
* @type {import("@sveltejs/kit").RequestHandler}
*/
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));
return new Response(JSON.stringify({
@ -44,3 +43,20 @@ export async function POST(event) {
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>