:)
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",
|
||||
"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",
|
||||
|
@ -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",
|
||||
|
@ -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>
|
@ -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>
|
||||
|
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 = {
|
||||
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 { 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"
|
||||
|
@ -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,
|
||||
};
|
||||
},
|
||||
}
|
||||
};
|
@ -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> -->
|
||||
|
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";
|
||||
|
||||
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 */
|
||||
|
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
|
||||
});
|
||||
|
||||
redirect(302, "/");
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
redirect(302, "/");
|
||||
}
|
||||
|
||||
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