work
This commit is contained in:
parent
d62cc6f6a4
commit
aa391a8dba
99
src/app.pcss
99
src/app.pcss
@ -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+ */
|
||||
}
|
@ -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, "/");
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -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, "/")
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
5
src/lib/components/organisms/player/state.js
Normal file
5
src/lib/components/organisms/player/state.js
Normal 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);
|
@ -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>
|
@ -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}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
})
|
||||
}
|
28
src/routes/group/[groupid]/+page.server.js
Normal file
28
src/routes/group/[groupid]/+page.server.js
Normal 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
|
||||
};
|
||||
}
|
57
src/routes/group/[groupid]/+page.svelte
Normal file
57
src/routes/group/[groupid]/+page.svelte
Normal 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>
|
Loading…
Reference in New Issue
Block a user