This commit is contained in:
Omer Sabic 2024-05-06 18:54:27 +02:00
parent cfc063ef28
commit fd544459e4
25 changed files with 515 additions and 149 deletions

39
package-lock.json generated
View File

@ -8,11 +8,12 @@
"name": "front-end",
"version": "0.0.1",
"dependencies": {
"bits-ui": "^0.21.4",
"bits-ui": "^0.21.7",
"clsx": "^2.1.1",
"formsnap": "^1.0.0",
"lucide-svelte": "^0.373.0",
"mode-watcher": "^0.3.0",
"svelte-markdown": "^0.4.1",
"svelte-radix": "^1.1.0",
"svelte-sonner": "^0.3.22",
"sveltekit-superforms": "^2.12.6",
@ -953,6 +954,11 @@
"optional": true,
"peer": true
},
"node_modules/@types/marked": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/@types/marked/-/marked-5.0.2.tgz",
"integrity": "sha512-OucS4KMHhFzhz27KxmWg7J+kIYqyqoW5kdIEI319hqARQQUTqhao3M/F+uFnDXD0Rg72iDDZxZNxq5gvctmLlg=="
},
"node_modules/@types/pug": {
"version": "2.0.10",
"resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.10.tgz",
@ -1124,9 +1130,9 @@
}
},
"node_modules/bits-ui": {
"version": "0.21.4",
"resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-0.21.4.tgz",
"integrity": "sha512-IL+7s19GW561jwkeYk23dwkTfQ9606I062qqv2AtjCdhhIdoOEJNVBX0kjP5xefSaS6ojL0HGG54att0aRTcAQ==",
"version": "0.21.7",
"resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-0.21.7.tgz",
"integrity": "sha512-1PKp90ly1R6jexIiAUj1Dk4u2pln7ok+L8Vc0rHMY7pi7YZvadFNZvkp1G5BtmL8qh2xsn4MVNgKjPAQMCxW0A==",
"dependencies": {
"@internationalized/date": "^3.5.1",
"@melt-ui/svelte": "0.76.2",
@ -1136,7 +1142,7 @@
"url": "https://github.com/sponsors/huntabyte"
},
"peerDependencies": {
"svelte": "^4.0.0"
"svelte": "^4.0.0 || ^5.0.0-next.118"
}
},
"node_modules/bits-ui/node_modules/nanoid": {
@ -1950,6 +1956,17 @@
"@jridgewell/sourcemap-codec": "^1.4.15"
}
},
"node_modules/marked": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/marked/-/marked-5.1.2.tgz",
"integrity": "sha512-ahRPGXJpjMjwSOlBoTMZAK7ATXkli5qCPxZ21TG44rx1KEo44bii4ekgTDQPNRQ4Kh7JMb9Ub1PVk1NxRSsorg==",
"bin": {
"marked": "bin/marked.js"
},
"engines": {
"node": ">= 16"
}
},
"node_modules/mdn-data": {
"version": "2.0.30",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",
@ -2974,6 +2991,18 @@
"svelte": "^3.19.0 || ^4.0.0"
}
},
"node_modules/svelte-markdown": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/svelte-markdown/-/svelte-markdown-0.4.1.tgz",
"integrity": "sha512-pOlLY6EruKJaWI9my/2bKX8PdTeP5CM0s4VMmwmC2prlOkjAf+AOmTM4wW/l19Y6WZ87YmP8+ZCJCCwBChWjYw==",
"dependencies": {
"@types/marked": "^5.0.1",
"marked": "^5.1.2"
},
"peerDependencies": {
"svelte": "^4.0.0"
}
},
"node_modules/svelte-preprocess": {
"version": "5.1.4",
"resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-5.1.4.tgz",

View File

@ -29,11 +29,12 @@
},
"type": "module",
"dependencies": {
"bits-ui": "^0.21.4",
"bits-ui": "^0.21.7",
"clsx": "^2.1.1",
"formsnap": "^1.0.0",
"lucide-svelte": "^0.373.0",
"mode-watcher": "^0.3.0",
"svelte-markdown": "^0.4.1",
"svelte-radix": "^1.1.0",
"svelte-sonner": "^0.3.22",
"sveltekit-superforms": "^2.12.6",

View File

@ -1,4 +1,7 @@
<script>
import { Badge } from "$lib/components/ui/badge";
let className = "";
export {className as class};
</script>
<Badge class="ml-auto bg-purple-500 text-white hover:bg-purple-700">PRO</Badge>
<Badge class="ml-auto bg-purple-500 text-white hover:bg-purple-700 {className}">PRO</Badge>

View File

@ -5,11 +5,14 @@
export let tip = "";
export let variant = "";
export let size = "";
let className = "";
export {className as class};
</script>
<Tooltip.Root>
<Tooltip.Trigger asChild let:builder>
<Button builders={[builder]} {variant} {size}><slot /></Button>
<Button class={className} builders={[builder]} {variant} {size} on:click><slot /></Button>
</Tooltip.Trigger>
<Tooltip.Content>
<p>{tip}</p>

View File

@ -0,0 +1,6 @@
import Root from "./switch.svelte";
export {
Root,
//
Root as Switch,
};

View File

@ -0,0 +1,24 @@
<script>
import { Switch as SwitchPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let className = undefined;
export let checked = undefined;
export { className as class };
</script>
<SwitchPrimitive.Root
bind:checked
class={cn(
"peer inline-flex h-[24px] w-[44px] shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className
)}
{...$$restProps}
on:click
on:keydown
>
<SwitchPrimitive.Thumb
class={cn(
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitive.Root>

View File

@ -0,0 +1,6 @@
import Root from "./textarea.svelte";
export {
Root,
//
Root as Textarea,
};

View File

@ -0,0 +1,29 @@
<script>
import { cn } from "$lib/utils.js";
let className = undefined;
export let value = undefined;
export { className as class };
export let readonly = undefined;
</script>
<textarea
class={cn(
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
bind:value
{readonly}
on:blur
on:change
on:click
on:focus
on:keydown
on:keypress
on:keyup
on:mouseover
on:mouseenter
on:mouseleave
on:paste
on:input
{...$$restProps}
></textarea>

View File

@ -1,3 +1,3 @@
export const config = {
api_url: "http://api.omersabic.com:3001"
api_url: "http://localhost:3000"
}

View File

@ -14,10 +14,18 @@
import { Button } from '$lib/components/ui/button/index.js';
import { ModeWatcher, toggleMode } from 'mode-watcher';
import { Toaster } from "$lib/components/ui/sonner";
import { Toaster } from '$lib/components/ui/sonner';
/** @type {import('./$types').LayoutServerData} */
export let data;
const navs = [
{ name: 'Dashboard', redirect: '/' },
{ name: 'Articles', redirect: '/articles' },
{ name: 'Emails', redirect: '/emails' },
{ name: 'Website', redirect: '##' },
{ name: 'Analytics', redirect: '##' },
];
</script>
<Toaster />
@ -32,19 +40,9 @@
<Package2 class="h-6 w-6" />
<span class="sr-only">{data.me.user.name}</span>
</a>
<a href="/" class="text-foreground transition-colors hover:text-foreground"> Dashboard </a>
<a href="/articles" class="text-muted-foreground transition-colors hover:text-foreground">
Articles
</a>
<a href="##" class="text-muted-foreground transition-colors hover:text-foreground">
Emails
</a>
<a href="##" class="text-muted-foreground transition-colors hover:text-foreground">
Website
</a>
<a href="##" class="text-muted-foreground transition-colors hover:text-foreground">
Analytics
</a>
{#each navs as nav}
<a href="{nav.redirect}" class="text-muted-foreground transition-colors hover:text-foreground">{nav.name}</a>
{/each}
</nav>
<Sheet.Root>
<Sheet.Trigger asChild let:builder>
@ -59,12 +57,9 @@
<Package2 class="h-6 w-6" />
<span class="sr-only">{data.me.user.name}</span>
</a>
<a href="/" class="hover:text-foreground"> Dashboard </a>
<a href="/articles" class="text-muted-foreground hover:text-foreground"> Articles </a>
<a href="##" class="text-muted-foreground hover:text-foreground"> Products </a>
<a href="##" class="text-muted-foreground hover:text-foreground"> Customers </a>
<a href="##" class="text-muted-foreground hover:text-foreground"> Analytics </a>
{#each navs as nav}
<a href="{nav.redirect}" class="hover:text-foreground">{nav.name}</a>
{/each}
<div class="mt-auto">
<Button on:click={toggleMode} variant="outline" size="icon">
<Sun
@ -81,7 +76,7 @@
</Sheet.Root>
<div class="flex w-full items-center gap-4 md:ml-auto md:gap-2 lg:gap-4">
<div class="hidden md:block ml-auto">
<div class="ml-auto hidden md:block">
<Button on:click={toggleMode} variant="outline" size="icon">
<Sun
class="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0"

View File

@ -18,6 +18,7 @@ export const load = async ({ fetch }) => {
const dataVideos = await videosRes.json();
return {
articles: dataBlog.articles,
site: dataBlog.site,
videos: dataVideos.videos,
form: await superValidate(zod(formSchema)),
}
@ -49,5 +50,5 @@ export const actions = {
return {
form,
};
},
}
};

View File

@ -1,19 +1,15 @@
<script>
import * as Table from '$lib/components/ui/table/index.js';
import { ExternalLink, Pen, Trash } from 'lucide-svelte';
import * as Dialog from '$lib/components/ui/dialog';
import * as Select from '$lib/components/ui/select';
import * as Form from '$lib/components/ui/form';
import { Button, buttonVariants } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import TooltipButton from '$lib/components/molecules/tooltipbutton.svelte';
import ProBadge from '$lib/components/molecules/probadge.svelte';
import { formSchema } from './schema';
import { superForm } from 'sveltekit-superforms';
import { zodClient } from 'sveltekit-superforms/adapters';
import { toast } from 'svelte-sonner';
import CreateArticleDialog from './createArticleDialog.svelte';
import EditArticleDialog from './editArticleDialog.svelte';
/** @type {import("./$types").PageData} */
export let data;
@ -23,146 +19,61 @@
validators: zodClient(formSchema)
});
const { form: formData, enhance } = form;
function submitArticle() {
isDialogOpen = false;
toast('Article is queued for generation.');
}
/** @type {object | null} */
let editingContent = null;
/**
* @param {string} id
*/
function editArticle(id) {
fetch("/articles/getArticleBody?id=" + id).then(x=>x.json()).then(data => {
editingContent = data.article;
});
}
</script>
<div class="mx-auto w-full max-w-[1000px]">
<Dialog.Root bind:open={isDialogOpen}>
<Dialog.Trigger class={buttonVariants({ variant: 'default' })}>Create Article</Dialog.Trigger>
<Dialog.Content class="w-full sm:max-w-[750px]">
<Dialog.Header>
<Dialog.Title>Create Article</Dialog.Title>
<Dialog.Description>
Configure your article and let our AI do the writing!
</Dialog.Description>
</Dialog.Header>
<form
method="POST"
use:enhance
name="blog-converter"
id="blog-converter"
on:submit={submitArticle}
>
<Form.Field {form} name="video_id">
<Form.Control let:attrs>
<div class="grid gap-4 py-4">
<div class="grid grid-cols-4 items-center gap-4">
<Form.Label for="video_id" class="text-right">Youtube video*</Form.Label>
<!-- <Input
id="youtube_url"
placeholder="www.youtube.com/watch?v=..."
class="col-span-3"
/> -->
<Select.Root>
<Select.Trigger class="w-[300px]" {...attrs}>
<Select.Value />
</Select.Trigger>
<Select.Content>
<Select.Group>
{#each data.videos as video}
<Select.Item
value={video.snippet.resourceId.videoId}
label={video.snippet.title}>{video.snippet.title}</Select.Item
>
{/each}
</Select.Group>
</Select.Content>
<Select.Input bind:value={$formData.video_id} name={attrs.name} />
</Select.Root>
</div>
<!-- <div class="grid grid-cols-4 items-center gap-4">
<Form.Label for="length" class="text-right">Article length</Form.Label>
<Select.Root portal={null} name="length">
<Select.Trigger class="w-[300px]">
<Select.Value />
</Select.Trigger>
<Select.Content>
<Select.Group>
<Select.Item value="700" label="Short (~700 words)"
>Short (~700 words)</Select.Item
>
<Select.Item value="1500" label="Medium (~1500 words)"
>Medium (~1500 words)<ProBadge /></Select.Item
>
<Select.Item value="2500" label="Long (~2500 words)"
>Long (~2500 words)<ProBadge /></Select.Item
>
</Select.Group>
</Select.Content>
<Select.Input {...attrs} />
</Select.Root>
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Form.Label for="format" class="text-right">Format</Form.Label>
<Select.Root portal={null} name="format">
<Select.Trigger class="w-[200px]">
<Select.Value />
</Select.Trigger>
<Select.Content>
<Select.Group>
<Select.Item value="summary" label="Summary">Summary</Select.Item>
<Select.Item value="listicle" label="Listicle">Listicle</Select.Item>
<Select.Item value="product review" label="Product Review"
>Product Review</Select.Item
>
<Select.Item value="news report" label="News Report">News Report</Select.Item>
<Select.Item value="tutorial" label="Tutorial">Tutorial</Select.Item>
</Select.Group>
</Select.Content>
<Select.Input {...attrs}/>
</Select.Root>
</div> -->
</div>
</Form.Control>
<Form.Description />
<Form.FieldErrors />
</Form.Field>
</form>
<Dialog.Footer>
<Form.Button type="submit" form="blog-converter">Create</Form.Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>
<CreateArticleDialog bind:open={isDialogOpen} {form} videos={data.videos} on:submit={submitArticle} />
<Table.Root>
<Table.Caption>A list of your recent articles.</Table.Caption>
<Table.Header>
<Table.Row>
<Table.Head class="w-[100px]">ID</Table.Head>
<Table.Head>Title</Table.Head>
<Table.Head class="w-[25px]">ID</Table.Head>
<Table.Head class="max-w-[300px]">Title</Table.Head>
<Table.Head class="text-end">Source</Table.Head>
<Table.Head class="text-end">Actions</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
<!-- {#each data.articles as article, i (i)}
{#each data.articles as article, i (i)}
<Table.Row>
<Table.Cell class="font-medium">{article.id.slice(0,8)}</Table.Cell>
<Table.Cell class="w-fill overflow-hidden overflow-ellipsis text-nowrap"
<Table.Cell class="font-medium">{i+1}</Table.Cell>
<Table.Cell class="max-w-[300px] overflow-hidden overflow-ellipsis text-nowrap"
>{article.title}</Table.Cell
>
<Table.Cell class="text-end">{"Youtube"}</Table.Cell>
<Table.Cell class="w-fit text-end">
<TooltipButton variant="outline" size="icon" tip="Preview">
<TooltipButton class="hover:bg-blue-600" variant="outline" size="icon" tip="Preview" on:click={() => window.open("/site/" + data.site.id + "/" + article.seo_slug)}>
<ExternalLink size="1rem" />
</TooltipButton>
<TooltipButton variant="outline" size="icon" tip="Edit">
<TooltipButton variant="outline" size="icon" tip="Edit" on:click={() => editArticle(article.id)}>
<Pen size="1rem" />
</TooltipButton>
<TooltipButton variant="outline" size="icon" tip="Delete">
<TooltipButton class="hover:bg-red-600" variant="outline" size="icon" tip="Delete">
<Trash size="1rem" />
</TooltipButton>
</Table.Cell>
</Table.Row>
{/each} -->
{/each}
</Table.Body>
</Table.Root>
</div>
<EditArticleDialog bind:article_data={editingContent} />
<!-- <p>{JSON.stringify(data)}</p> -->

View File

@ -0,0 +1,120 @@
<script>
import ProBadge from "$lib/components/molecules/probadge.svelte";
import { buttonVariants } from "$lib/components/ui/button";
import * as Dialog from "$lib/components/ui/dialog";
import * as Form from "$lib/components/ui/form";
import * as Select from "$lib/components/ui/select";
import { Switch } from "$lib/components/ui/switch";
export let videos;
export let form;
export let open = false;
const { form: formData, enhance } = form;
</script>
<Dialog.Root bind:open={open}>
<Dialog.Trigger class={buttonVariants({ variant: 'default' })}>Create Article</Dialog.Trigger>
<Dialog.Content class="w-full sm:max-w-[750px]">
<Dialog.Header>
<Dialog.Title>Create Article</Dialog.Title>
<Dialog.Description>
Configure your article and let our AI do the writing!
</Dialog.Description>
</Dialog.Header>
<form
method="POST"
use:enhance
name="blog-converter"
id="blog-converter"
on:submit
>
<Form.Field {form} name="video_id">
<Form.Control let:attrs>
<div class="grid gap-4 py-4">
<div class="grid grid-cols-4 items-center gap-4">
<Form.Label for="video_id" class="text-right">Youtube video*</Form.Label>
<!-- <Input
id="youtube_url"
placeholder="www.youtube.com/watch?v=..."
class="col-span-3"
/> -->
<Select.Root>
<Select.Trigger class="w-[300px]" {...attrs}>
<Select.Value />
</Select.Trigger>
<Select.Content>
<Select.Group>
{#each videos as video}
<Select.Item
value={video.snippet.resourceId.videoId}
label={video.snippet.title}>{video.snippet.title}</Select.Item
>
{/each}
</Select.Group>
</Select.Content>
<Select.Input bind:value={$formData.video_id} name={attrs.name} />
</Select.Root>
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Form.Label for="length" class="text-right">Article length</Form.Label>
<Select.Root portal={null} name="length">
<Select.Trigger class="w-[300px]">
<Select.Value />
</Select.Trigger>
<Select.Content>
<Select.Group>
<Select.Item value="700" label="Short (~700 words)"
>Short (~700 words)</Select.Item
>
<Select.Item value="1500" label="Medium (~1500 words)"
>Medium (~1500 words)<ProBadge /></Select.Item
>
<Select.Item value="2500" label="Long (~2500 words)"
>Long (~2500 words)<ProBadge /></Select.Item
>
</Select.Group>
</Select.Content>
<Select.Input {...attrs} />
</Select.Root>
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Form.Label for="format" class="text-right">Format</Form.Label>
<Select.Root portal={null} name="format">
<Select.Trigger class="w-[200px]">
<Select.Value />
</Select.Trigger>
<Select.Content>
<Select.Group>
<Select.Item value="summary" label="Summary">Summary</Select.Item>
<Select.Item value="listicle" label="Listicle">Listicle</Select.Item>
<Select.Item value="product review" label="Product Review"
>Product Review</Select.Item
>
<Select.Item value="news report" label="News Report">News Report</Select.Item>
<Select.Item value="tutorial" label="Tutorial">Tutorial</Select.Item>
</Select.Group>
</Select.Content>
<Select.Input {...attrs}/>
</Select.Root>
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Form.Label for="faq-switch" class="text-right">Include FAQ</Form.Label>
<div class="flex items-center justify-start">
<Switch id="faq-switch" name="faq" />
<ProBadge class="ml-[10px]" />
</div>
</div>
</div>
</Form.Control>
<Form.Description />
<Form.FieldErrors />
</Form.Field>
</form>
<Dialog.Footer>
<Form.Button type="submit" form="blog-converter">Create</Form.Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>

View File

@ -0,0 +1,55 @@
<script>
import { Button } from '$lib/components/ui/button';
import * as Dialog from '$lib/components/ui/dialog';
import * as Form from '$lib/components/ui/form';
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
import { Switch } from '$lib/components/ui/switch';
import { Textarea } from '$lib/components/ui/textarea';
/** @type {object | null} */
export let article_data;
/**
* @param {any} e
*/
function updateArticle(e) {
console.log(e.target.value);
}
$: {
console.log(article_data);
}
</script>
<Dialog.Root open={article_data !== null} on:closed={() => (article_data = null)}>
<Dialog.Content class="w-full sm:max-w-[750px]">
<Dialog.Header>
<Dialog.Title>Edit Article</Dialog.Title>
<Dialog.Description></Dialog.Description>
</Dialog.Header>
<form id="blog_editor" on:submit|preventDefault={updateArticle}>
<div>
<Label for="is_public_switch">Public</Label>
<Switch name="is_public" id="is_public_switch" checked={article_data?.is_public} />
</div>
<div>
<Input value={article_data?.title} name="" />
</div>
<div>
<Textarea name="blog-editor" id="" value={article_data?.content} class="h-80" />
<p class="text-sm text-muted-foreground">
Manually edit your article using <a
href="https://www.markdownguide.org/basic-syntax"
class="font-bold underline">Markdown</a
>
</p>
</div>
</form>
<Dialog.Footer>
<Button type="submit" form="blog_editor">Submit</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>

View File

@ -0,0 +1,11 @@
import { config } from '$lib';
import { error, json, text } from '@sveltejs/kit';
/** @type {import('./$types').RequestHandler} */
export async function GET(event) {
if(!event.url.searchParams.has("id")) return error(400);
const id = event.url.searchParams.get("id");
const article = await fetch(config.api_url + "/blog/article?id=" + id).then(x=>x.json());
return json(article);
}

View File

@ -1,9 +1,17 @@
import { z } from "zod";
export const formSchema = z.object({
export const createFormSchema = z.object({
video_id: z.string(),
// length: z.number().optional(),
// format: z.enum(["summary", "listicle", "product review", "news report", "tutorial"]).optional(),
});
/** @typedef {typeof formSchema} FormSchema */
/** @typedef {typeof createFormSchema} CreateFormSchema */
export const editFormSchema = z.object({
video_id: z.string(),
// length: z.number().optional(),
// format: z.enum(["summary", "listicle", "product review", "news report", "tutorial"]).optional(),
});
/** @typedef {typeof editFormSchema} EditFormSchema */

View File

@ -0,0 +1,13 @@
import { config } from "$lib";
/** @type {import("./$types").PageServerLoad} */
export const load = async ({ fetch }) => {
const signupsRes = await fetch(config.api_url + "/blog/signups", {
credentials: 'include'
});
const signupsData = await signupsRes.json();
return {
signups: signupsData.signups
}
}

View File

@ -0,0 +1,45 @@
<script>
import * as Table from '$lib/components/ui/table/index.js';
import { ExternalLink, Pen, Trash } from 'lucide-svelte';
import TooltipButton from '$lib/components/molecules/tooltipbutton.svelte';
/** @type {import("./$types").PageData} */
export let data;
</script>
<div class="mx-auto w-full max-w-[1000px]">
<Table.Root>
<Table.Caption>A list of recent signups on your website.</Table.Caption>
<Table.Header>
<Table.Row>
<Table.Head class="max-w-[300px]">Email</Table.Head>
<Table.Head class="text-end">Date</Table.Head>
<Table.Head class="text-end">Actions</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#each data.signups as signup, i (i)}
<Table.Row>
<Table.Cell class="max-w-[300px] overflow-hidden overflow-ellipsis text-nowrap"
>{signup.email}</Table.Cell
>
<Table.Cell class="text-end">{new Date(signup.created_at).toLocaleDateString()}</Table.Cell>
<!-- <Table.Cell class="w-fit text-end">
<TooltipButton class="hover:bg-blue-600" variant="outline" size="icon" tip="Preview" on:click={() => window.open("/site/" + data.site.id + "/" + article.seo_slug)}>
<ExternalLink size="1rem" />
</TooltipButton>
<TooltipButton variant="outline" size="icon" tip="Edit">
<Pen size="1rem" />
</TooltipButton>
<TooltipButton class="hover:bg-red-600" variant="outline" size="icon" tip="Delete">
<Trash size="1rem" />
</TooltipButton>
</Table.Cell> -->
</Table.Row>
{/each}
</Table.Body>
</Table.Root>
</div>
<!-- <p>{JSON.stringify(data)}</p> -->

View File

@ -22,10 +22,10 @@ export const load = async ({ request, cookies }) => {
secure: false
});
redirect(302, "/");
} catch (e) {
console.log(e);
}
redirect(302, "/");
}
return {

View File

@ -0,0 +1,45 @@
import { config } from "$lib";
import { error } from "@sveltejs/kit";
import { articles } from "./store";
import { fail, superValidate } from "sveltekit-superforms";
import { zod } from "sveltekit-superforms/adapters";
import { formSchema } from "./schema";
/** @type {import("./$types").PageServerLoad} */
export async function load(event) {
const data = await fetch(config.api_url + "/blog?mine=false&blog_id=" + event.params.site_id).then(x=>x.json());
if(!data.success) error(404);
articles.set(data.articles);
return {
...{articles: data.articles, site: data.site}
}
}
/** @type {import("@sveltejs/kit").Actions} */
export const actions = {
default: async (event) => {
const form = await superValidate(event, zod(formSchema));
if (!form.valid) {
return fail(400, {
form,
});
}
const res = await event.fetch(config.api_url + "/blog/signup", {
method: "POST",
body: JSON.stringify({
email: form.data.email,
site_id: event.params.site_id
}),
headers: {
"content-type": "application/json"
}
});
return {
form,
};
},
};

View File

@ -0,0 +1,27 @@
<script>
import { browser } from '$app/environment';
import { onMount } from 'svelte';
let currentPath = "";
$: currentPath = browser && window.location.pathname || "/"
export let data;
</script>
{data.site.name}
<ul>
{#each data.articles as article}
<li>
<a href="{currentPath + "/" + article.seo_slug}">{article.title}</a>
</li>
{/each}
</ul>
<div>
<form method="post">
<h4>sign up to our newsletter!</h4>
<input type="email" name="email">
<button type="submit">Sign up!</button>
</form>
</div>

View File

@ -0,0 +1,14 @@
import { config } from "$lib";
import { error } from "@sveltejs/kit";
import { articles } from "../store";
/** @type {import("./$types").PageServerLoad} */
export async function load(event) {
const result = await fetch(config.api_url + "/blog/article?id=" + event.params.article_id).then(x=>x.json());
if(!result.article) error(404)
return {
...result
}
}

View File

@ -0,0 +1,10 @@
<script>
import SvelteMarkdown from 'svelte-markdown'
export let data;
</script>
<h1>
{data.article.title}
</h1>
<SvelteMarkdown source={data.article.content} />

View File

@ -0,0 +1,7 @@
import { z } from "zod";
export const formSchema = z.object({
email: z.string().email().min(1),
});
/** @typedef {typeof formSchema} FormSchema */

View File

@ -0,0 +1,3 @@
import { writable } from "svelte/store";
export let articles = writable([]);