:)
This commit is contained in:
parent
cfc063ef28
commit
fd544459e4
39
package-lock.json
generated
39
package-lock.json
generated
@ -8,11 +8,12 @@
|
|||||||
"name": "front-end",
|
"name": "front-end",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bits-ui": "^0.21.4",
|
"bits-ui": "^0.21.7",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"formsnap": "^1.0.0",
|
"formsnap": "^1.0.0",
|
||||||
"lucide-svelte": "^0.373.0",
|
"lucide-svelte": "^0.373.0",
|
||||||
"mode-watcher": "^0.3.0",
|
"mode-watcher": "^0.3.0",
|
||||||
|
"svelte-markdown": "^0.4.1",
|
||||||
"svelte-radix": "^1.1.0",
|
"svelte-radix": "^1.1.0",
|
||||||
"svelte-sonner": "^0.3.22",
|
"svelte-sonner": "^0.3.22",
|
||||||
"sveltekit-superforms": "^2.12.6",
|
"sveltekit-superforms": "^2.12.6",
|
||||||
@ -953,6 +954,11 @@
|
|||||||
"optional": true,
|
"optional": true,
|
||||||
"peer": 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": {
|
"node_modules/@types/pug": {
|
||||||
"version": "2.0.10",
|
"version": "2.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.10.tgz",
|
||||||
@ -1124,9 +1130,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/bits-ui": {
|
"node_modules/bits-ui": {
|
||||||
"version": "0.21.4",
|
"version": "0.21.7",
|
||||||
"resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-0.21.4.tgz",
|
"resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-0.21.7.tgz",
|
||||||
"integrity": "sha512-IL+7s19GW561jwkeYk23dwkTfQ9606I062qqv2AtjCdhhIdoOEJNVBX0kjP5xefSaS6ojL0HGG54att0aRTcAQ==",
|
"integrity": "sha512-1PKp90ly1R6jexIiAUj1Dk4u2pln7ok+L8Vc0rHMY7pi7YZvadFNZvkp1G5BtmL8qh2xsn4MVNgKjPAQMCxW0A==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@internationalized/date": "^3.5.1",
|
"@internationalized/date": "^3.5.1",
|
||||||
"@melt-ui/svelte": "0.76.2",
|
"@melt-ui/svelte": "0.76.2",
|
||||||
@ -1136,7 +1142,7 @@
|
|||||||
"url": "https://github.com/sponsors/huntabyte"
|
"url": "https://github.com/sponsors/huntabyte"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"svelte": "^4.0.0"
|
"svelte": "^4.0.0 || ^5.0.0-next.118"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/bits-ui/node_modules/nanoid": {
|
"node_modules/bits-ui/node_modules/nanoid": {
|
||||||
@ -1950,6 +1956,17 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.15"
|
"@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": {
|
"node_modules/mdn-data": {
|
||||||
"version": "2.0.30",
|
"version": "2.0.30",
|
||||||
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",
|
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",
|
||||||
@ -2974,6 +2991,18 @@
|
|||||||
"svelte": "^3.19.0 || ^4.0.0"
|
"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": {
|
"node_modules/svelte-preprocess": {
|
||||||
"version": "5.1.4",
|
"version": "5.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-5.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-5.1.4.tgz",
|
||||||
|
@ -29,11 +29,12 @@
|
|||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bits-ui": "^0.21.4",
|
"bits-ui": "^0.21.7",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"formsnap": "^1.0.0",
|
"formsnap": "^1.0.0",
|
||||||
"lucide-svelte": "^0.373.0",
|
"lucide-svelte": "^0.373.0",
|
||||||
"mode-watcher": "^0.3.0",
|
"mode-watcher": "^0.3.0",
|
||||||
|
"svelte-markdown": "^0.4.1",
|
||||||
"svelte-radix": "^1.1.0",
|
"svelte-radix": "^1.1.0",
|
||||||
"svelte-sonner": "^0.3.22",
|
"svelte-sonner": "^0.3.22",
|
||||||
"sveltekit-superforms": "^2.12.6",
|
"sveltekit-superforms": "^2.12.6",
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { Badge } from "$lib/components/ui/badge";
|
import { Badge } from "$lib/components/ui/badge";
|
||||||
|
let className = "";
|
||||||
|
|
||||||
|
export {className as class};
|
||||||
</script>
|
</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>
|
@ -5,11 +5,14 @@
|
|||||||
export let tip = "";
|
export let tip = "";
|
||||||
export let variant = "";
|
export let variant = "";
|
||||||
export let size = "";
|
export let size = "";
|
||||||
|
let className = "";
|
||||||
|
|
||||||
|
export {className as class};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Tooltip.Root>
|
<Tooltip.Root>
|
||||||
<Tooltip.Trigger asChild let:builder>
|
<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.Trigger>
|
||||||
<Tooltip.Content>
|
<Tooltip.Content>
|
||||||
<p>{tip}</p>
|
<p>{tip}</p>
|
||||||
|
6
src/lib/components/ui/switch/index.js
Normal file
6
src/lib/components/ui/switch/index.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import Root from "./switch.svelte";
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
//
|
||||||
|
Root as Switch,
|
||||||
|
};
|
24
src/lib/components/ui/switch/switch.svelte
Normal file
24
src/lib/components/ui/switch/switch.svelte
Normal 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>
|
6
src/lib/components/ui/textarea/index.js
Normal file
6
src/lib/components/ui/textarea/index.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import Root from "./textarea.svelte";
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
//
|
||||||
|
Root as Textarea,
|
||||||
|
};
|
29
src/lib/components/ui/textarea/textarea.svelte
Normal file
29
src/lib/components/ui/textarea/textarea.svelte
Normal 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>
|
@ -1,3 +1,3 @@
|
|||||||
export const config = {
|
export const config = {
|
||||||
api_url: "http://api.omersabic.com:3001"
|
api_url: "http://localhost:3000"
|
||||||
}
|
}
|
@ -14,10 +14,18 @@
|
|||||||
import { Button } from '$lib/components/ui/button/index.js';
|
import { Button } from '$lib/components/ui/button/index.js';
|
||||||
|
|
||||||
import { ModeWatcher, toggleMode } from 'mode-watcher';
|
import { ModeWatcher, toggleMode } from 'mode-watcher';
|
||||||
import { Toaster } from "$lib/components/ui/sonner";
|
import { Toaster } from '$lib/components/ui/sonner';
|
||||||
|
|
||||||
/** @type {import('./$types').LayoutServerData} */
|
/** @type {import('./$types').LayoutServerData} */
|
||||||
export let data;
|
export let data;
|
||||||
|
|
||||||
|
const navs = [
|
||||||
|
{ name: 'Dashboard', redirect: '/' },
|
||||||
|
{ name: 'Articles', redirect: '/articles' },
|
||||||
|
{ name: 'Emails', redirect: '/emails' },
|
||||||
|
{ name: 'Website', redirect: '##' },
|
||||||
|
{ name: 'Analytics', redirect: '##' },
|
||||||
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Toaster />
|
<Toaster />
|
||||||
@ -32,19 +40,9 @@
|
|||||||
<Package2 class="h-6 w-6" />
|
<Package2 class="h-6 w-6" />
|
||||||
<span class="sr-only">{data.me.user.name}</span>
|
<span class="sr-only">{data.me.user.name}</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="/" class="text-foreground transition-colors hover:text-foreground"> Dashboard </a>
|
{#each navs as nav}
|
||||||
<a href="/articles" class="text-muted-foreground transition-colors hover:text-foreground">
|
<a href="{nav.redirect}" class="text-muted-foreground transition-colors hover:text-foreground">{nav.name}</a>
|
||||||
Articles
|
{/each}
|
||||||
</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>
|
|
||||||
</nav>
|
</nav>
|
||||||
<Sheet.Root>
|
<Sheet.Root>
|
||||||
<Sheet.Trigger asChild let:builder>
|
<Sheet.Trigger asChild let:builder>
|
||||||
@ -59,12 +57,9 @@
|
|||||||
<Package2 class="h-6 w-6" />
|
<Package2 class="h-6 w-6" />
|
||||||
<span class="sr-only">{data.me.user.name}</span>
|
<span class="sr-only">{data.me.user.name}</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="/" class="hover:text-foreground"> Dashboard </a>
|
{#each navs as nav}
|
||||||
<a href="/articles" class="text-muted-foreground hover:text-foreground"> Articles </a>
|
<a href="{nav.redirect}" class="hover:text-foreground">{nav.name}</a>
|
||||||
<a href="##" class="text-muted-foreground hover:text-foreground"> Products </a>
|
{/each}
|
||||||
<a href="##" class="text-muted-foreground hover:text-foreground"> Customers </a>
|
|
||||||
<a href="##" class="text-muted-foreground hover:text-foreground"> Analytics </a>
|
|
||||||
|
|
||||||
<div class="mt-auto">
|
<div class="mt-auto">
|
||||||
<Button on:click={toggleMode} variant="outline" size="icon">
|
<Button on:click={toggleMode} variant="outline" size="icon">
|
||||||
<Sun
|
<Sun
|
||||||
@ -81,7 +76,7 @@
|
|||||||
</Sheet.Root>
|
</Sheet.Root>
|
||||||
|
|
||||||
<div class="flex w-full items-center gap-4 md:ml-auto md:gap-2 lg:gap-4">
|
<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">
|
<Button on:click={toggleMode} variant="outline" size="icon">
|
||||||
<Sun
|
<Sun
|
||||||
class="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0"
|
class="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0"
|
||||||
|
@ -18,6 +18,7 @@ export const load = async ({ fetch }) => {
|
|||||||
const dataVideos = await videosRes.json();
|
const dataVideos = await videosRes.json();
|
||||||
return {
|
return {
|
||||||
articles: dataBlog.articles,
|
articles: dataBlog.articles,
|
||||||
|
site: dataBlog.site,
|
||||||
videos: dataVideos.videos,
|
videos: dataVideos.videos,
|
||||||
form: await superValidate(zod(formSchema)),
|
form: await superValidate(zod(formSchema)),
|
||||||
}
|
}
|
||||||
@ -49,5 +50,5 @@ export const actions = {
|
|||||||
return {
|
return {
|
||||||
form,
|
form,
|
||||||
};
|
};
|
||||||
},
|
}
|
||||||
};
|
};
|
@ -1,19 +1,15 @@
|
|||||||
<script>
|
<script>
|
||||||
import * as Table from '$lib/components/ui/table/index.js';
|
import * as Table from '$lib/components/ui/table/index.js';
|
||||||
import { ExternalLink, Pen, Trash } from 'lucide-svelte';
|
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 TooltipButton from '$lib/components/molecules/tooltipbutton.svelte';
|
||||||
import ProBadge from '$lib/components/molecules/probadge.svelte';
|
|
||||||
|
|
||||||
import { formSchema } from './schema';
|
import { formSchema } from './schema';
|
||||||
import { superForm } from 'sveltekit-superforms';
|
import { superForm } from 'sveltekit-superforms';
|
||||||
import { zodClient } from 'sveltekit-superforms/adapters';
|
import { zodClient } from 'sveltekit-superforms/adapters';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
|
import CreateArticleDialog from './createArticleDialog.svelte';
|
||||||
|
import EditArticleDialog from './editArticleDialog.svelte';
|
||||||
|
|
||||||
/** @type {import("./$types").PageData} */
|
/** @type {import("./$types").PageData} */
|
||||||
export let data;
|
export let data;
|
||||||
@ -23,146 +19,61 @@
|
|||||||
validators: zodClient(formSchema)
|
validators: zodClient(formSchema)
|
||||||
});
|
});
|
||||||
|
|
||||||
const { form: formData, enhance } = form;
|
|
||||||
|
|
||||||
function submitArticle() {
|
function submitArticle() {
|
||||||
isDialogOpen = false;
|
isDialogOpen = false;
|
||||||
toast('Article is queued for generation.');
|
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>
|
</script>
|
||||||
|
|
||||||
<div class="mx-auto w-full max-w-[1000px]">
|
<div class="mx-auto w-full max-w-[1000px]">
|
||||||
<Dialog.Root bind:open={isDialogOpen}>
|
<CreateArticleDialog bind:open={isDialogOpen} {form} videos={data.videos} on:submit={submitArticle} />
|
||||||
<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>
|
|
||||||
<Table.Root>
|
<Table.Root>
|
||||||
<Table.Caption>A list of your recent articles.</Table.Caption>
|
<Table.Caption>A list of your recent articles.</Table.Caption>
|
||||||
<Table.Header>
|
<Table.Header>
|
||||||
<Table.Row>
|
<Table.Row>
|
||||||
<Table.Head class="w-[100px]">ID</Table.Head>
|
<Table.Head class="w-[25px]">ID</Table.Head>
|
||||||
<Table.Head>Title</Table.Head>
|
<Table.Head class="max-w-[300px]">Title</Table.Head>
|
||||||
<Table.Head class="text-end">Source</Table.Head>
|
<Table.Head class="text-end">Source</Table.Head>
|
||||||
<Table.Head class="text-end">Actions</Table.Head>
|
<Table.Head class="text-end">Actions</Table.Head>
|
||||||
</Table.Row>
|
</Table.Row>
|
||||||
</Table.Header>
|
</Table.Header>
|
||||||
<Table.Body>
|
<Table.Body>
|
||||||
<!-- {#each data.articles as article, i (i)}
|
{#each data.articles as article, i (i)}
|
||||||
<Table.Row>
|
<Table.Row>
|
||||||
<Table.Cell class="font-medium">{article.id.slice(0,8)}</Table.Cell>
|
<Table.Cell class="font-medium">{i+1}</Table.Cell>
|
||||||
<Table.Cell class="w-fill overflow-hidden overflow-ellipsis text-nowrap"
|
<Table.Cell class="max-w-[300px] overflow-hidden overflow-ellipsis text-nowrap"
|
||||||
>{article.title}</Table.Cell
|
>{article.title}</Table.Cell
|
||||||
>
|
>
|
||||||
<Table.Cell class="text-end">{"Youtube"}</Table.Cell>
|
<Table.Cell class="text-end">{"Youtube"}</Table.Cell>
|
||||||
<Table.Cell class="w-fit text-end">
|
<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" />
|
<ExternalLink size="1rem" />
|
||||||
</TooltipButton>
|
</TooltipButton>
|
||||||
<TooltipButton variant="outline" size="icon" tip="Edit">
|
<TooltipButton variant="outline" size="icon" tip="Edit" on:click={() => editArticle(article.id)}>
|
||||||
<Pen size="1rem" />
|
<Pen size="1rem" />
|
||||||
</TooltipButton>
|
</TooltipButton>
|
||||||
<TooltipButton variant="outline" size="icon" tip="Delete">
|
<TooltipButton class="hover:bg-red-600" variant="outline" size="icon" tip="Delete">
|
||||||
<Trash size="1rem" />
|
<Trash size="1rem" />
|
||||||
</TooltipButton>
|
</TooltipButton>
|
||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
</Table.Row>
|
</Table.Row>
|
||||||
{/each} -->
|
{/each}
|
||||||
</Table.Body>
|
</Table.Body>
|
||||||
</Table.Root>
|
</Table.Root>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<EditArticleDialog bind:article_data={editingContent} />
|
||||||
|
|
||||||
<!-- <p>{JSON.stringify(data)}</p> -->
|
<!-- <p>{JSON.stringify(data)}</p> -->
|
||||||
|
120
src/routes/(app)/articles/createArticleDialog.svelte
Normal file
120
src/routes/(app)/articles/createArticleDialog.svelte
Normal 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>
|
55
src/routes/(app)/articles/editArticleDialog.svelte
Normal file
55
src/routes/(app)/articles/editArticleDialog.svelte
Normal 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>
|
11
src/routes/(app)/articles/getArticleBody/+server.js
Normal file
11
src/routes/(app)/articles/getArticleBody/+server.js
Normal 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);
|
||||||
|
}
|
@ -1,9 +1,17 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
export const formSchema = z.object({
|
export const createFormSchema = z.object({
|
||||||
video_id: z.string(),
|
video_id: z.string(),
|
||||||
// length: z.number().optional(),
|
// length: z.number().optional(),
|
||||||
// format: z.enum(["summary", "listicle", "product review", "news report", "tutorial"]).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 */
|
||||||
|
13
src/routes/(app)/emails/+page.server.js
Normal file
13
src/routes/(app)/emails/+page.server.js
Normal 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
|
||||||
|
}
|
||||||
|
}
|
45
src/routes/(app)/emails/+page.svelte
Normal file
45
src/routes/(app)/emails/+page.svelte
Normal 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> -->
|
@ -22,10 +22,10 @@ export const load = async ({ request, cookies }) => {
|
|||||||
secure: false
|
secure: false
|
||||||
});
|
});
|
||||||
|
|
||||||
redirect(302, "/");
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e);
|
console.log(e);
|
||||||
}
|
}
|
||||||
|
redirect(302, "/");
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
45
src/routes/site/[site_id]/+page.server.js
Normal file
45
src/routes/site/[site_id]/+page.server.js
Normal 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,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
27
src/routes/site/[site_id]/+page.svelte
Normal file
27
src/routes/site/[site_id]/+page.svelte
Normal 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>
|
14
src/routes/site/[site_id]/[article_id]/+page.server.js
Normal file
14
src/routes/site/[site_id]/[article_id]/+page.server.js
Normal 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
|
||||||
|
}
|
||||||
|
}
|
10
src/routes/site/[site_id]/[article_id]/+page.svelte
Normal file
10
src/routes/site/[site_id]/[article_id]/+page.svelte
Normal 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} />
|
7
src/routes/site/[site_id]/schema.js
Normal file
7
src/routes/site/[site_id]/schema.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const formSchema = z.object({
|
||||||
|
email: z.string().email().min(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
/** @typedef {typeof formSchema} FormSchema */
|
3
src/routes/site/[site_id]/store.js
Normal file
3
src/routes/site/[site_id]/store.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import { writable } from "svelte/store";
|
||||||
|
|
||||||
|
export let articles = writable([]);
|
Loading…
Reference in New Issue
Block a user