whoaaaaa lots of stuff changed

This commit is contained in:
Omer Sabic 2024-05-12 22:04:43 +02:00
parent 3f8ba248cc
commit 193113e029
48 changed files with 1395 additions and 503 deletions

186
package-lock.json generated
View File

@ -14,6 +14,7 @@
"lucide-svelte": "^0.373.0",
"mode-watcher": "^0.3.0",
"svelte-markdown": "^0.4.1",
"svelte-motion": "^0.12.2",
"svelte-radix": "^1.1.0",
"svelte-sonner": "^0.3.22",
"sveltekit-superforms": "^2.12.6",
@ -25,6 +26,7 @@
"@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"@tabler/icons-svelte": "^3.3.0",
"autoprefixer": "^10.4.16",
"postcss": "^8.4.32",
"postcss-load-config": "^5.0.2",
@ -32,8 +34,11 @@
"prettier-plugin-svelte": "^3.1.2",
"prettier-plugin-tailwindcss": "^0.5.9",
"svelte": "^4.2.7",
"svelte-awesome-color-picker": "^3.0.6",
"svelte-check": "^3.6.0",
"svelte-legos": "^0.2.2",
"tailwindcss": "^3.3.6",
"timeago.js": "^4.0.2",
"typescript": "^5.0.0",
"vite": "^5.0.3"
}
@ -937,6 +942,32 @@
"tslib": "^2.4.0"
}
},
"node_modules/@tabler/icons": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.3.0.tgz",
"integrity": "sha512-PLVe9d7b59sKytbx00KgeGhQG3N176Ezv8YMmsnSz4s0ifDzMWlp/h2wEfQZ0ZNe8e377GY2OW6kovUe3Rnd0g==",
"dev": true,
"funding": {
"type": "github",
"url": "https://github.com/sponsors/codecalm"
}
},
"node_modules/@tabler/icons-svelte": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/@tabler/icons-svelte/-/icons-svelte-3.3.0.tgz",
"integrity": "sha512-1NJvaT6w68j3y1c8lJSohRFKzpRT4LcTK7pPW1kGN7JXPJfh2efIw705dA0jxuwRClMrTxZt8BmEd6TRKQMqtA==",
"dev": true,
"dependencies": {
"@tabler/icons": "3.3.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/codecalm"
},
"peerDependencies": {
"svelte": ">=3 <5"
}
},
"node_modules/@types/cookie": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
@ -959,12 +990,26 @@
"resolved": "https://registry.npmjs.org/@types/marked/-/marked-5.0.2.tgz",
"integrity": "sha512-OucS4KMHhFzhz27KxmWg7J+kIYqyqoW5kdIEI319hqARQQUTqhao3M/F+uFnDXD0Rg72iDDZxZNxq5gvctmLlg=="
},
"node_modules/@types/prop-types": {
"version": "15.7.12",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz",
"integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q=="
},
"node_modules/@types/pug": {
"version": "2.0.10",
"resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.10.tgz",
"integrity": "sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==",
"dev": true
},
"node_modules/@types/react": {
"version": "18.3.2",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.2.tgz",
"integrity": "sha512-Btgg89dAnqD4vV7R3hlwOxgqobUQKgx3MmrQRi0yYbs/P0ym8XozIAlkqVilPqHQwXs4e9Tf63rrCgl58BcO4w==",
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.0.2"
}
},
"node_modules/@types/validator": {
"version": "13.11.9",
"resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.9.tgz",
@ -1280,6 +1325,16 @@
}
]
},
"node_modules/canvas-confetti": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/canvas-confetti/-/canvas-confetti-1.9.3.tgz",
"integrity": "sha512-rFfTURMvmVEX1gyXFgn5QMn81bYk70qa0HLzcIOSVEyl57n6o9ItHeBtUSWdvKAPY0xlvBHno4/v3QPrT83q9g==",
"dev": true,
"funding": {
"type": "donate",
"url": "https://www.paypal.me/kirilvatev"
}
},
"node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
@ -1339,6 +1394,12 @@
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"node_modules/colord": {
"version": "2.9.3",
"resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz",
"integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==",
"dev": true
},
"node_modules/commander": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
@ -1397,6 +1458,11 @@
"node": ">=4"
}
},
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
},
"node_modules/dayjs": {
"version": "1.11.10",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz",
@ -1663,6 +1729,19 @@
"url": "https://github.com/sponsors/rawify"
}
},
"node_modules/framesync": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/framesync/-/framesync-6.1.2.tgz",
"integrity": "sha512-jBTqhX6KaQVDyus8muwZbBeGGP0XgujBRbQ7gM7BRdS3CadCZIHiawyzYLnafYcvZIh5j8WE7cxZKFn7dXhu9g==",
"dependencies": {
"tslib": "2.4.0"
}
},
"node_modules/framesync/node_modules/tslib": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
},
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@ -1748,6 +1827,11 @@
"node": ">= 0.4"
}
},
"node_modules/hey-listen": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/hey-listen/-/hey-listen-1.0.8.tgz",
"integrity": "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q=="
},
"node_modules/import-fresh": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@ -2254,6 +2338,22 @@
"node": ">= 6"
}
},
"node_modules/popmotion": {
"version": "11.0.5",
"resolved": "https://registry.npmjs.org/popmotion/-/popmotion-11.0.5.tgz",
"integrity": "sha512-la8gPM1WYeFznb/JqF4GiTkRRPZsfaj2+kCxqQgr2MJylMmIKUwBfWW8Wa5fml/8gmtlD5yI01MP1QCZPWmppA==",
"dependencies": {
"framesync": "6.1.2",
"hey-listen": "^1.0.8",
"style-value-types": "5.1.2",
"tslib": "2.4.0"
}
},
"node_modules/popmotion/node_modules/tslib": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
},
"node_modules/postcss": {
"version": "8.4.38",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz",
@ -2484,6 +2584,21 @@
}
}
},
"node_modules/prism-svelte": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/prism-svelte/-/prism-svelte-0.5.0.tgz",
"integrity": "sha512-db91Bf3pRGKDPz1lAqLFSJXeW13mulUJxhycysFpfXV5MIK7RgWWK2E5aPAa71s8TCzQUXxF5JOV42/iOs6QkA==",
"dev": true
},
"node_modules/prismjs": {
"version": "1.29.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz",
"integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==",
"dev": true,
"engines": {
"node": ">=6"
}
},
"node_modules/property-expr": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz",
@ -2850,6 +2965,20 @@
"node": ">=8"
}
},
"node_modules/style-value-types": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/style-value-types/-/style-value-types-5.1.2.tgz",
"integrity": "sha512-Vs9fNreYF9j6W2VvuDTP7kepALi7sk0xtk2Tu8Yxi9UoajJdEVpNpCov0HsLTqXvNGKX+Uv09pkozVITi1jf3Q==",
"dependencies": {
"hey-listen": "^1.0.8",
"tslib": "2.4.0"
}
},
"node_modules/style-value-types/node_modules/tslib": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
},
"node_modules/sucrase": {
"version": "3.35.0",
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
@ -2958,6 +3087,28 @@
"node": ">=16"
}
},
"node_modules/svelte-awesome-color-picker": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/svelte-awesome-color-picker/-/svelte-awesome-color-picker-3.0.6.tgz",
"integrity": "sha512-lkyT9+78mpvzUm6yZLh2RKGHJn9U/XOZsc+WrCZVPaNTRnleled3dwR+EX0SgiAeFV7V/ieK3bu4wpgU0HRFlQ==",
"dev": true,
"dependencies": {
"colord": "^2.9.3",
"svelte-awesome-slider": "^1.1.0"
},
"peerDependencies": {
"svelte": "^3.55.1 || ^4.0.0"
}
},
"node_modules/svelte-awesome-slider": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/svelte-awesome-slider/-/svelte-awesome-slider-1.1.0.tgz",
"integrity": "sha512-MgY6ZdBON42HVZqNWNjq2HOgyDlC35q0TNbV/YO1l1/bcb5yhM8EE97h1AJ/7F6t6sLzXhQ3qPf5nGyCdDSnCg==",
"dev": true,
"peerDependencies": {
"svelte": "^3.54.0 || ^4.0.0"
}
},
"node_modules/svelte-check": {
"version": "3.6.9",
"resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-3.6.9.tgz",
@ -2991,6 +3142,20 @@
"svelte": "^3.19.0 || ^4.0.0"
}
},
"node_modules/svelte-legos": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/svelte-legos/-/svelte-legos-0.2.2.tgz",
"integrity": "sha512-HTVkCIqhrxdy+OpXjxGr/4xIJEGv4d2cRQwTjm0SYfLw/YF1I1l/TQR59nb2WvjccnO8TNFNTvAWP5pgXQnU+w==",
"dev": true,
"dependencies": {
"canvas-confetti": "^1.6.0",
"prism-svelte": "^0.5.0",
"prismjs": "^1.29.0"
},
"peerDependencies": {
"svelte": "^4.0.0"
}
},
"node_modules/svelte-markdown": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/svelte-markdown/-/svelte-markdown-0.4.1.tgz",
@ -3003,6 +3168,21 @@
"svelte": "^4.0.0"
}
},
"node_modules/svelte-motion": {
"version": "0.12.2",
"resolved": "https://registry.npmjs.org/svelte-motion/-/svelte-motion-0.12.2.tgz",
"integrity": "sha512-7RrdRz9iVP55B9HT/C0hYW3pyrKlF61kAby/AkDtOAP0uHFQDrfd0qQetDC81cEsK9b40jt+jfcqSAXcA7LPEw==",
"dependencies": {
"@types/react": "^18.2.42",
"framesync": "^6.1.2",
"popmotion": "^11.0.5",
"style-value-types": "5.1.2",
"tslib": "^2.6.2"
},
"peerDependencies": {
"svelte": ">=3.35.0 || ^4.0.0 || ^5.0.0 || ^5.0.0-next.0"
}
},
"node_modules/svelte-preprocess": {
"version": "5.1.4",
"resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-5.1.4.tgz",
@ -3313,6 +3493,12 @@
"node": ">=0.8"
}
},
"node_modules/timeago.js": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/timeago.js/-/timeago.js-4.0.2.tgz",
"integrity": "sha512-a7wPxPdVlQL7lqvitHGGRsofhdwtkoSXPGATFuSOA2i1ZNQEPLrGnj68vOp2sOJTCFAQVXPeNMX/GctBaO9L2w==",
"dev": true
},
"node_modules/tiny-case": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz",

View File

@ -15,6 +15,7 @@
"@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"@tabler/icons-svelte": "^3.3.0",
"autoprefixer": "^10.4.16",
"postcss": "^8.4.32",
"postcss-load-config": "^5.0.2",
@ -22,8 +23,11 @@
"prettier-plugin-svelte": "^3.1.2",
"prettier-plugin-tailwindcss": "^0.5.9",
"svelte": "^4.2.7",
"svelte-awesome-color-picker": "^3.0.6",
"svelte-check": "^3.6.0",
"svelte-legos": "^0.2.2",
"tailwindcss": "^3.3.6",
"timeago.js": "^4.0.2",
"typescript": "^5.0.0",
"vite": "^5.0.3"
},
@ -35,6 +39,7 @@
"lucide-svelte": "^0.373.0",
"mode-watcher": "^0.3.0",
"svelte-markdown": "^0.4.1",
"svelte-motion": "^0.12.2",
"svelte-radix": "^1.1.0",
"svelte-sonner": "^0.3.22",
"sveltekit-superforms": "^2.12.6",

View File

@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<body data-sveltekit-preload-data="hover" style="height: 100vh">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

View File

@ -0,0 +1,116 @@
<script lang="ts">
import { cn } from '$lib/utils';
import { M, Motion } from 'svelte-motion';
export let className: string | undefined = undefined;
const paths = [
'M-380 -189C-380 -189 -312 216 152 343C616 470 684 875 684 875',
'M-366 -205C-366 -205 -298 200 166 327C630 454 698 859 698 859',
'M-352 -221C-352 -221 -284 184 180 311C644 438 712 843 712 843',
'M-338 -237C-338 -237 -270 168 194 295C658 422 726 827 726 827',
'M-324 -253C-324 -253 -256 152 208 279C672 406 740 811 740 811',
'M-310 -269C-310 -269 -242 136 222 263C686 390 754 795 754 795',
'M-296 -285C-296 -285 -228 120 236 247C700 374 768 779 768 779',
'M-282 -301C-282 -301 -214 104 250 231C714 358 782 763 782 763',
'M-268 -317C-268 -317 -200 88 264 215C728 342 796 747 796 747',
'M-254 -333C-254 -333 -186 72 278 199C742 326 810 731 810 731',
'M-240 -349C-240 -349 -172 56 292 183C756 310 824 715 824 715',
'M-226 -365C-226 -365 -158 40 306 167C770 294 838 699 838 699',
'M-212 -381C-212 -381 -144 24 320 151C784 278 852 683 852 683',
'M-198 -397C-198 -397 -130 8 334 135C798 262 866 667 866 667',
'M-184 -413C-184 -413 -116 -8 348 119C812 246 880 651 880 651',
'M-170 -429C-170 -429 -102 -24 362 103C826 230 894 635 894 635',
'M-156 -445C-156 -445 -88 -40 376 87C840 214 908 619 908 619',
'M-142 -461C-142 -461 -74 -56 390 71C854 198 922 603 922 603',
'M-128 -477C-128 -477 -60 -72 404 55C868 182 936 587 936 587',
'M-114 -493C-114 -493 -46 -88 418 39C882 166 950 571 950 571',
'M-100 -509C-100 -509 -32 -104 432 23C896 150 964 555 964 555',
'M-86 -525C-86 -525 -18 -120 446 7C910 134 978 539 978 539',
'M-72 -541C-72 -541 -4 -136 460 -9C924 118 992 523 992 523',
'M-58 -557C-58 -557 10 -152 474 -25C938 102 1006 507 1006 507',
'M-44 -573C-44 -573 24 -168 488 -41C952 86 1020 491 1020 491'
];
</script>
<div
class={cn(
'absolute inset-0 flex h-full w-full items-center justify-center [mask-repeat:no-repeat] [mask-size:40px]',
className
)}
>
<svg
class=" pointer-events-none absolute z-0 h-full w-full"
width="100%"
height="100%"
viewBox="0 0 696 316"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M-380 -189C-380 -189 -312 216 152 343C616 470 684 875 684 875M-373 -197C-373 -197 -305 208 159 335C623 462 691 867 691 867M-366 -205C-366 -205 -298 200 166 327C630 454 698 859 698 859M-359 -213C-359 -213 -291 192 173 319C637 446 705 851 705 851M-352 -221C-352 -221 -284 184 180 311C644 438 712 843 712 843M-345 -229C-345 -229 -277 176 187 303C651 430 719 835 719 835M-338 -237C-338 -237 -270 168 194 295C658 422 726 827 726 827M-331 -245C-331 -245 -263 160 201 287C665 414 733 819 733 819M-324 -253C-324 -253 -256 152 208 279C672 406 740 811 740 811M-317 -261C-317 -261 -249 144 215 271C679 398 747 803 747 803M-310 -269C-310 -269 -242 136 222 263C686 390 754 795 754 795M-303 -277C-303 -277 -235 128 229 255C693 382 761 787 761 787M-296 -285C-296 -285 -228 120 236 247C700 374 768 779 768 779M-289 -293C-289 -293 -221 112 243 239C707 366 775 771 775 771M-282 -301C-282 -301 -214 104 250 231C714 358 782 763 782 763M-275 -309C-275 -309 -207 96 257 223C721 350 789 755 789 755M-268 -317C-268 -317 -200 88 264 215C728 342 796 747 796 747M-261 -325C-261 -325 -193 80 271 207C735 334 803 739 803 739M-254 -333C-254 -333 -186 72 278 199C742 326 810 731 810 731M-247 -341C-247 -341 -179 64 285 191C749 318 817 723 817 723M-240 -349C-240 -349 -172 56 292 183C756 310 824 715 824 715M-233 -357C-233 -357 -165 48 299 175C763 302 831 707 831 707M-226 -365C-226 -365 -158 40 306 167C770 294 838 699 838 699M-219 -373C-219 -373 -151 32 313 159C777 286 845 691 845 691M-212 -381C-212 -381 -144 24 320 151C784 278 852 683 852 683M-205 -389C-205 -389 -137 16 327 143C791 270 859 675 859 675M-198 -397C-198 -397 -130 8 334 135C798 262 866 667 866 667M-191 -405C-191 -405 -123 0 341 127C805 254 873 659 873 659M-184 -413C-184 -413 -116 -8 348 119C812 246 880 651 880 651M-177 -421C-177 -421 -109 -16 355 111C819 238 887 643 887 643M-170 -429C-170 -429 -102 -24 362 103C826 230 894 635 894 635M-163 -437C-163 -437 -95 -32 369 95C833 222 901 627 901 627M-156 -445C-156 -445 -88 -40 376 87C840 214 908 619 908 619M-149 -453C-149 -453 -81 -48 383 79C847 206 915 611 915 611M-142 -461C-142 -461 -74 -56 390 71C854 198 922 603 922 603M-135 -469C-135 -469 -67 -64 397 63C861 190 929 595 929 595M-128 -477C-128 -477 -60 -72 404 55C868 182 936 587 936 587M-121 -485C-121 -485 -53 -80 411 47C875 174 943 579 943 579M-114 -493C-114 -493 -46 -88 418 39C882 166 950 571 950 571M-107 -501C-107 -501 -39 -96 425 31C889 158 957 563 957 563M-100 -509C-100 -509 -32 -104 432 23C896 150 964 555 964 555M-93 -517C-93 -517 -25 -112 439 15C903 142 971 547 971 547M-86 -525C-86 -525 -18 -120 446 7C910 134 978 539 978 539M-79 -533C-79 -533 -11 -128 453 -1C917 126 985 531 985 531M-72 -541C-72 -541 -4 -136 460 -9C924 118 992 523 992 523M-65 -549C-65 -549 3 -144 467 -17C931 110 999 515 999 515M-58 -557C-58 -557 10 -152 474 -25C938 102 1006 507 1006 507M-51 -565C-51 -565 17 -160 481 -33C945 94 1013 499 1013 499M-44 -573C-44 -573 24 -168 488 -41C952 86 1020 491 1020 491M-37 -581C-37 -581 31 -176 495 -49C959 78 1027 483 1027 483M-30 -589C-30 -589 38 -184 502 -57C966 70 1034 475 1034 475M-23 -597C-23 -597 45 -192 509 -65C973 62 1041 467 1041 467M-16 -605C-16 -605 52 -200 516 -73C980 54 1048 459 1048 459M-9 -613C-9 -613 59 -208 523 -81C987 46 1055 451 1055 451M-2 -621C-2 -621 66 -216 530 -89C994 38 1062 443 1062 443M5 -629C5 -629 73 -224 537 -97C1001 30 1069 435 1069 435M12 -637C12 -637 80 -232 544 -105C1008 22 1076 427 1076 427M19 -645C19 -645 87 -240 551 -113C1015 14 1083 419 1083 419"
stroke="url(#paint0_radial_242_278)"
stroke-opacity="0.05"
stroke-width="0.5"
></path>
{#each paths as path, index (index)}
<Motion isSVG={true} let:motion>
<path
use:motion
d={path}
stroke={`url(#linearGradient-${index})`}
stroke-opacity="0.4"
stroke-width="0.5"
></path>
</Motion>
{/each}
<defs>
{#each paths as path, index (`gradient-${index}`)}
<Motion
isSVG={true}
let:motion
animate={{
x1: ['0%', '100%'],
x2: ['0%', '95%'],
y1: ['0%', '100%'],
y2: ['0%', `${93 + Math.random() * 8}%`]
}}
transition={{
duration: Math.random() * 10 + 10,
ease: 'easeInOut',
repeat: Infinity,
delay: Math.random() * 10
}}
>
<linearGradient
use:motion
id={`linearGradient-${index}`}
x1="100%"
x2="100%"
y1="100%"
y2="100%"
>
<stop stop-color="#18CCFC" stop-opacity="0"></stop>
<stop stop-color="#18CCFC"></stop>
<stop offset="32.5%" stop-color="#6344F5"></stop>
<stop offset="100%" stop-color="#AE48FF" stop-opacity="0"></stop>
</linearGradient>
</Motion>
{/each}
<radialGradient
id="paint0_radial_242_278"
cx="0"
cy="0"
r="1"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(352 34) rotate(90) scale(555 1560.62)"
>
<stop offset="0.0666667" stop-color="var(--neutral-300)"></stop>
<stop offset="0.243243" stop-color="var(--neutral-300)"></stop>
<stop offset="0.43594" stop-color="white" stop-opacity="0"></stop>
</radialGradient>
</defs>
</svg>
</div>

View File

@ -0,0 +1,24 @@
import Root from "./pagination.svelte";
import Content from "./pagination-content.svelte";
import Item from "./pagination-item.svelte";
import Link from "./pagination-link.svelte";
import PrevButton from "./pagination-prev-button.svelte";
import NextButton from "./pagination-next-button.svelte";
import Ellipsis from "./pagination-ellipsis.svelte";
export {
Root,
Content,
Item,
Link,
PrevButton,
NextButton,
Ellipsis,
//
Root as Pagination,
Content as PaginationContent,
Item as PaginationItem,
Link as PaginationLink,
PrevButton as PaginationPrevButton,
NextButton as PaginationNextButton,
Ellipsis as PaginationEllipsis,
};

View File

@ -0,0 +1,9 @@
<script>
import { cn } from "$lib/utils.js";
let className = undefined;
export { className as class };
</script>
<ul class={cn("flex flex-row items-center gap-1", className)} {...$$restProps}>
<slot />
</ul>

View File

@ -0,0 +1,15 @@
<script>
import Ellipsis from "lucide-svelte/icons/ellipsis";
import { cn } from "$lib/utils.js";
let className = undefined;
export { className as class };
</script>
<span
aria-hidden
class={cn("flex h-9 w-9 items-center justify-center", className)}
{...$$restProps}
>
<Ellipsis class="h-4 w-4" />
<span class="sr-only">More pages</span>
</span>

View File

@ -0,0 +1,9 @@
<script>
import { cn } from "$lib/utils.js";
let className = undefined;
export { className as class };
</script>
<li class={cn("", className)} {...$$restProps}>
<slot />
</li>

View File

@ -0,0 +1,25 @@
<script>
import { Pagination as PaginationPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import { buttonVariants } from "$lib/components/ui/button/index.js";
let className = undefined;
export let page;
export let size = "icon";
export let isActive = false;
export { className as class };
</script>
<PaginationPrimitive.Page
bind:page
class={cn(
buttonVariants({
variant: isActive ? "outline" : "ghost",
size,
}),
className
)}
{...$$restProps}
on:click
>
<slot>{page.value}</slot>
</PaginationPrimitive.Page>

View File

@ -0,0 +1,23 @@
<script>
import { Pagination as PaginationPrimitive } from "bits-ui";
import ChevronRight from "lucide-svelte/icons/chevron-right";
import { Button } from "$lib/components/ui/button/index.js";
import { cn } from "$lib/utils.js";
let className = undefined;
export { className as class };
</script>
<PaginationPrimitive.NextButton asChild let:builder>
<Button
variant="ghost"
class={cn("gap-1 pr-2.5", className)}
builders={[builder]}
on:click
{...$$restProps}
>
<slot>
<span>Next</span>
<ChevronRight class="h-4 w-4" />
</slot>
</Button>
</PaginationPrimitive.NextButton>

View File

@ -0,0 +1,23 @@
<script>
import { Pagination as PaginationPrimitive } from "bits-ui";
import ChevronLeft from "lucide-svelte/icons/chevron-left";
import { Button } from "$lib/components/ui/button/index.js";
import { cn } from "$lib/utils.js";
let className = undefined;
export { className as class };
</script>
<PaginationPrimitive.PrevButton asChild let:builder>
<Button
variant="ghost"
class={cn("gap-1 pl-2.5", className)}
builders={[builder]}
on:click
{...$$restProps}
>
<slot>
<ChevronLeft class="h-4 w-4" />
<span>Previous</span>
</slot>
</Button>
</PaginationPrimitive.PrevButton>

View File

@ -0,0 +1,27 @@
<script>
import { Pagination as PaginationPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let className = undefined;
export let count = 0;
export let perPage = 10;
export let page = 1;
export let siblingCount = 1;
export { className as class };
$: currentPage = page;
</script>
<PaginationPrimitive.Root
{count}
{perPage}
{siblingCount}
bind:page
let:builder
let:pages
let:range
asChild
{...$$restProps}
>
<nav {...builder} class={cn("mx-auto flex w-full flex-col items-center", className)}>
<slot {pages} {range} {currentPage} />
</nav>
</PaginationPrimitive.Root>

View File

@ -2,6 +2,11 @@ import { clsx } from "clsx";
import { twMerge } from "tailwind-merge";
import { cubicOut } from "svelte/easing";
/**
*
* @param {import("clsx").ClassValue[]} inputs
* @returns
*/
export function cn(...inputs) {
return twMerge(clsx(inputs));
}

View File

@ -1,229 +0,0 @@
<script lang="ts">
import Activity from 'lucide-svelte/icons/activity';
import ArrowUpRight from 'lucide-svelte/icons/arrow-up-right';
import CreditCard from 'lucide-svelte/icons/credit-card';
import DollarSign from 'lucide-svelte/icons/dollar-sign';
import Users from 'lucide-svelte/icons/users';
import * as Avatar from '$lib/components/ui/avatar/index.js';
import { Badge } from '$lib/components/ui/badge/index.js';
import { Button } from '$lib/components/ui/button/index.js';
import * as Card from '$lib/components/ui/card/index.js';
import * as Table from '$lib/components/ui/table/index.js';
</script>
<div class="grid gap-4 md:grid-cols-2 md:gap-8 lg:grid-cols-4">
<Card.Root>
<Card.Header class="flex flex-row items-center justify-between space-y-0 pb-2">
<Card.Title class="text-sm font-medium">Total Articles</Card.Title>
<DollarSign class="h-4 w-4 text-muted-foreground" />
</Card.Header>
<Card.Content>
<div class="text-2xl font-bold">48</div>
<p class="text-xs text-muted-foreground">7 remaining this month</p>
</Card.Content>
</Card.Root>
<Card.Root>
<Card.Header class="flex flex-row items-center justify-between space-y-0 pb-2">
<Card.Title class="text-sm font-medium">Subscriptions</Card.Title>
<Users class="h-4 w-4 text-muted-foreground" />
</Card.Header>
<Card.Content>
<div class="text-2xl font-bold">+2350</div>
<p class="text-xs text-muted-foreground">+180.1% from last month</p>
</Card.Content>
</Card.Root>
<Card.Root>
<Card.Header class="flex flex-row items-center justify-between space-y-0 pb-2">
<Card.Title class="text-sm font-medium">Emails collected</Card.Title>
<CreditCard class="h-4 w-4 text-muted-foreground" />
</Card.Header>
<Card.Content>
<div class="text-2xl font-bold">+12,234</div>
<p class="text-xs text-muted-foreground">+19% from last month</p>
</Card.Content>
</Card.Root>
<Card.Root>
<Card.Header class="flex flex-row items-center justify-between space-y-0 pb-2">
<Card.Title class="text-sm font-medium">Reads</Card.Title>
<Activity class="h-4 w-4 text-muted-foreground" />
</Card.Header>
<Card.Content>
<div class="text-2xl font-bold">+573</div>
<p class="text-xs text-muted-foreground">+201 since last hour</p>
</Card.Content>
</Card.Root>
</div>
<div class="grid gap-4 md:gap-8 lg:grid-cols-2 xl:grid-cols-3">
<Card.Root class="xl:col-span-2">
<Card.Header class="flex flex-row items-center">
<div class="grid gap-2">
<Card.Title>Articles</Card.Title>
<Card.Description>Recent articles written from your youtube videos.</Card.Description>
</div>
<Button href="##" size="sm" class="ml-auto gap-1">
View All
<ArrowUpRight class="h-4 w-4" />
</Button>
</Card.Header>
<Card.Content>
<Table.Root>
<Table.Header>
<Table.Row>
<Table.Head>Customer</Table.Head>
<Table.Head class="xl:table.-column hidden">Type</Table.Head>
<Table.Head class="xl:table.-column hidden">Status</Table.Head>
<Table.Head class="xl:table.-column hidden">Date</Table.Head>
<Table.Head class="text-right">Amount</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
<Table.Row>
<Table.Cell>
<div class="font-medium">Liam Johnson</div>
<div class="hidden text-sm text-muted-foreground md:inline">liam@example.com</div>
</Table.Cell>
<Table.Cell class="xl:table.-column hidden">Sale</Table.Cell>
<Table.Cell class="xl:table.-column hidden">
<Badge class="text-xs" variant="outline">Approved</Badge>
</Table.Cell>
<Table.Cell class="md:table.-cell xl:table.-column hidden lg:hidden">
2023-06-23
</Table.Cell>
<Table.Cell class="text-right">$250.00</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<div class="font-medium">Olivia Smith</div>
<div class="hidden text-sm text-muted-foreground md:inline">olivia@example.com</div>
</Table.Cell>
<Table.Cell class="xl:table.-column hidden">Refund</Table.Cell>
<Table.Cell class="xl:table.-column hidden">
<Badge class="text-xs" variant="outline">Declined</Badge>
</Table.Cell>
<Table.Cell class="md:table.-cell xl:table.-column hidden lg:hidden">
2023-06-24
</Table.Cell>
<Table.Cell class="text-right">$150.00</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<div class="font-medium">Noah Williams</div>
<div class="hidden text-sm text-muted-foreground md:inline">noah@example.com</div>
</Table.Cell>
<Table.Cell class="xl:table.-column hidden">Subscription</Table.Cell>
<Table.Cell class="xl:table.-column hidden">
<Badge class="text-xs" variant="outline">Approved</Badge>
</Table.Cell>
<Table.Cell class="md:table.-cell xl:table.-column hidden lg:hidden">
2023-06-25
</Table.Cell>
<Table.Cell class="text-right">$350.00</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<div class="font-medium">Emma Brown</div>
<div class="hidden text-sm text-muted-foreground md:inline">emma@example.com</div>
</Table.Cell>
<Table.Cell class="xl:table.-column hidden">Sale</Table.Cell>
<Table.Cell class="xl:table.-column hidden">
<Badge class="text-xs" variant="outline">Approved</Badge>
</Table.Cell>
<Table.Cell class="md:table.-cell xl:table.-column hidden lg:hidden">
2023-06-26
</Table.Cell>
<Table.Cell class="text-right">$450.00</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<div class="font-medium">Liam Johnson</div>
<div class="hidden text-sm text-muted-foreground md:inline">liam@example.com</div>
</Table.Cell>
<Table.Cell class="xl:table.-column hidden">Sale</Table.Cell>
<Table.Cell class="xl:table.-column hidden">
<Badge class="text-xs" variant="outline">Approved</Badge>
</Table.Cell>
<Table.Cell class="md:table.-cell xl:table.-column hidden lg:hidden">
2023-06-27
</Table.Cell>
<Table.Cell class="text-right">$550.00</Table.Cell>
</Table.Row>
</Table.Body>
</Table.Root>
</Card.Content>
</Card.Root>
<Card.Root>
<Card.Header>
<Card.Title>Recent Signups</Card.Title>
</Card.Header>
<Card.Content class="grid gap-8">
<div class="flex items-center gap-4">
<Avatar.Root class="hidden h-9 w-9 sm:flex">
<Avatar.Image src="/avatars/01.png" alt="Avatar" />
<Avatar.Fallback>OM</Avatar.Fallback>
</Avatar.Root>
<div class="grid gap-1">
<p class="text-sm font-medium leading-none">Olivia Martin</p>
<p class="text-sm text-muted-foreground">olivia.martin@email.com</p>
</div>
<div class="ml-auto font-medium">56 seconds ago</div>
</div>
<div class="flex items-center gap-4">
<Avatar.Root class="hidden h-9 w-9 sm:flex">
<Avatar.Image src="/avatars/02.png" alt="Avatar" />
<Avatar.Fallback>JL</Avatar.Fallback>
</Avatar.Root>
<div class="grid gap-1">
<p class="text-sm font-medium leading-none">Jackson Lee</p>
<p class="text-sm text-muted-foreground">jackson.lee@email.com</p>
</div>
<div class="ml-auto font-medium">2 minutes ago</div>
</div>
<div class="flex items-center gap-4">
<Avatar.Root class="hidden h-9 w-9 sm:flex">
<Avatar.Image src="/avatars/03.png" alt="Avatar" />
<Avatar.Fallback>IN</Avatar.Fallback>
</Avatar.Root>
<div class="grid gap-1">
<p class="text-sm font-medium leading-none">Isabella Nguyen</p>
<p class="text-sm text-muted-foreground">isabella.nguyen@email.com</p>
</div>
<div class="ml-auto font-medium">5 minutes ago</div>
</div>
<div class="flex items-center gap-4">
<Avatar.Root class="hidden h-9 w-9 sm:flex">
<Avatar.Image src="/avatars/04.png" alt="Avatar" />
<Avatar.Fallback>WK</Avatar.Fallback>
</Avatar.Root>
<div class="grid gap-1">
<p class="text-sm font-medium leading-none">William Kim</p>
<p class="text-sm text-muted-foreground">will@email.com</p>
</div>
<div class="ml-auto font-medium">9 minutes ago</div>
</div>
<div class="flex items-center gap-4">
<Avatar.Root class="hidden h-9 w-9 sm:flex">
<Avatar.Image src="/avatars/05.png" alt="Avatar" />
<Avatar.Fallback>SD</Avatar.Fallback>
</Avatar.Root>
<div class="grid gap-1">
<p class="text-sm font-medium leading-none">Sofia Davis</p>
<p class="text-sm text-muted-foreground">sofia.davis@email.com</p>
</div>
<div class="ml-auto font-medium">15 minutes ago</div>
</div>
</Card.Content>
</Card.Root>
</div>
<!-- <p>{JSON.stringify(data)}</p>
{#await me}
<p>loading...</p>
{:then me}
{#if me && channel}
<p>Welcome back!</p>
<h3>Your channel:</h3>
<p>{channel.title}</p>
{:else}
<a href="{config.api_url}/auth/google">Log in here you fat bastard</a>
{/if}
{/await} -->

View File

@ -1,79 +0,0 @@
<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';
import { createFormSchema } 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;
let isDialogOpen = false;
const form = superForm(data.createForm, {
validators: zodClient(createFormSchema)
});
function submitArticle() {
isDialogOpen = false;
toast('Article is queued for generation.');
}
/** @type {import("./schema.js").ArticleData | 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]">
<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-[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)}
<Table.Row>
<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 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" on:click={() => editArticle(article.id)}>
<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>
<EditArticleDialog bind:article_data={editingContent} form={data.editForm} />
<!-- <p>{JSON.stringify(data)}</p> -->

View File

@ -1,121 +0,0 @@
<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
action="?/create"
>
<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

@ -1,6 +0,0 @@
<script>
import { config } from "$lib";
</script>
<a href="{config.api_url}/auth/google">Log in here you fat bastard</a>

39
src/routes/+page.svelte Normal file
View File

@ -0,0 +1,39 @@
<script>
import '../app.pcss';
import BackgroundBeams from '$lib/components/molecules/BackgroundBeams.svelte';
import { mediaQuery } from 'svelte-legos';
const isDesktop = mediaQuery('(min-width: 1024px)');
</script>
<div
class="relative flex h-screen w-full flex-col items-center justify-center bg-neutral-950 antialiased lg:px-32"
>
<div class="mx-auto max-w-2xl p-4">
<h2
class="relative z-10 bg-gradient-to-b mb-6 from-neutral-200 to-neutral-600 bg-clip-text text-center font-sans text-3xl font-bold text-transparent md:text-7xl"
>
Join the waitlist
</h2>
<p></p>
<p class="relative z-10 mx-auto my-2 max-w-lg text-center text-sm text-neutral-500">
Welcome to ExampleSaaS, the best solution for converting your video content into an engaging
blog website. Sign up to our waitlist to get notified when we launch and earn some free goodies as well.
</p>
<div class="flex flex-row gap-2 text-gray-50 pt-4">
<input
type="text"
placeholder="hi@example.com"
class="relative z-10 w-full rounded-lg border border-neutral-800 bg-neutral-950 p-2 placeholder:text-neutral-700 focus:ring-2 focus:ring-teal-500"
/>
<button class="relative z-10 inline-flex h-10 w-52 animate-shimmer items-center justify-center rounded-md border border-slate-800 bg-[linear-gradient(110deg,#000103,45%,#1e2631,55%,#000103)] bg-[length:200%_100%] px-6 font-medium text-slate-400 transition-colors focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 focus:ring-offset-slate-50">
Join Waitlist
</button>
</div>
</div>
{#if $isDesktop}
<BackgroundBeams />
{/if}
</div>

View File

@ -3,7 +3,10 @@ import { redirect } from "@sveltejs/kit";
/** @type {import("./$types").LayoutServerLoad} */
export const load = async ({fetch, cookies}) => {
if(!cookies.get("token")) return redirect(302, "/auth")
if(!cookies.get("token")) {
console.log("ruh roh no token")
return redirect(302, "/auth")
}
const res = await fetch(config.api_url+"/me")
if(res.status > 399 && res.status < 499) {
cookies.delete("token", {
@ -15,6 +18,6 @@ export const load = async ({fetch, cookies}) => {
const data = await res.json();
return {
me: data
me: data.user
}
}

View File

@ -1,5 +1,5 @@
<script>
import '../../app.pcss';
import '../../../app.pcss';
import Menu from 'lucide-svelte/icons/menu';
import Package2 from 'lucide-svelte/icons/package-2';
@ -20,12 +20,17 @@
export let data;
const navs = [
{ name: 'Dashboard', redirect: '/' },
{ name: 'Articles', redirect: '/articles' },
{ name: 'Emails', redirect: '/emails' },
{ name: 'Website', redirect: '##' },
{ name: 'Dashboard', redirect: '/app' },
{ name: 'Articles', redirect: '/app/articles' },
{ name: 'Emails', redirect: '/app/emails' },
{ name: 'Website', redirect: '/app/website' },
{ name: 'Analytics', redirect: '##' },
];
async function logout() {
fetch("/app/auth/logout");
window.location.href = "/"
}
</script>
<Toaster />
@ -38,7 +43,7 @@
>
<a href="##" class="flex items-center gap-2 text-lg font-semibold md:text-base">
<Package2 class="h-6 w-6" />
<span class="sr-only">{data.me.user.name}</span>
<span class="sr-only">{data.me.name}</span>
</a>
{#each navs as nav}
<a href="{nav.redirect}" class="text-muted-foreground transition-colors hover:text-foreground">{nav.name}</a>
@ -110,7 +115,7 @@
<DropdownMenu.Item>Settings</DropdownMenu.Item>
<DropdownMenu.Item>Support</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item>Logout</DropdownMenu.Item>
<DropdownMenu.Item on:click={logout}>Logout</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
</div>

View File

@ -0,0 +1,19 @@
import { config } from "$lib"
import { redirect } from "@sveltejs/kit";
/** @type {import("./$types").PageServerLoad} */
export const load = async ({fetch, cookies}) => {
const res = await fetch(config.api_url+"/dashboard")
if(res.status > 399 && res.status < 499) {
// cookies.delete("token", {
// path: "/"
// });
redirect(302, "/auth");
}
const data = await res.json();
return {
dashboard_info: data
}
}

View File

@ -0,0 +1,137 @@
<script>
import Activity from 'lucide-svelte/icons/activity';
import ArrowUpRight from 'lucide-svelte/icons/arrow-up-right';
import CreditCard from 'lucide-svelte/icons/credit-card';
import DollarSign from 'lucide-svelte/icons/dollar-sign';
import Users from 'lucide-svelte/icons/users';
import * as Avatar from '$lib/components/ui/avatar/index.js';
import { Button } from '$lib/components/ui/button/index.js';
import * as Card from '$lib/components/ui/card/index.js';
import * as Table from '$lib/components/ui/table/index.js';
import { format } from 'timeago.js';
/** @type {import("./$types").PageServerData} */
export let data;
</script>
<div class="grid gap-4 md:grid-cols-2 md:gap-8 lg:grid-cols-4">
<Card.Root>
<Card.Header class="flex flex-row items-center justify-between space-y-0 pb-2">
<Card.Title class="text-sm font-medium">Total Articles</Card.Title>
<DollarSign class="h-4 w-4 text-muted-foreground" />
</Card.Header>
<Card.Content>
<div class="text-2xl font-bold">48</div>
<p class="text-xs text-muted-foreground">7 remaining this month</p>
</Card.Content>
</Card.Root>
<Card.Root>
<Card.Header class="flex flex-row items-center justify-between space-y-0 pb-2">
<Card.Title class="text-sm font-medium">Subscriptions</Card.Title>
<Users class="h-4 w-4 text-muted-foreground" />
</Card.Header>
<Card.Content>
<div class="text-2xl font-bold">+2350</div>
<p class="text-xs text-muted-foreground">+180.1% from last month</p>
</Card.Content>
</Card.Root>
<Card.Root>
<Card.Header class="flex flex-row items-center justify-between space-y-0 pb-2">
<Card.Title class="text-sm font-medium">Emails collected</Card.Title>
<CreditCard class="h-4 w-4 text-muted-foreground" />
</Card.Header>
<Card.Content>
<div class="text-2xl font-bold">+573</div>
<p class="text-xs text-muted-foreground">+19% from last month</p>
</Card.Content>
</Card.Root>
<Card.Root>
<Card.Header class="flex flex-row items-center justify-between space-y-0 pb-2">
<Card.Title class="text-sm font-medium">Reads</Card.Title>
<Activity class="h-4 w-4 text-muted-foreground" />
</Card.Header>
<Card.Content>
<div class="text-2xl font-bold">+12,234</div>
<p class="text-xs text-muted-foreground">+201 since last hour</p>
</Card.Content>
</Card.Root>
</div>
<div class="grid gap-4 md:gap-8 lg:grid-cols-2 xl:grid-cols-3">
<Card.Root class="xl:col-span-2" style="height: min-content">
<Card.Header class="flex flex-row items-center">
<div class="grid gap-2">
<Card.Title>Articles</Card.Title>
<Card.Description>Recent articles written from your youtube videos.</Card.Description>
</div>
<Button href="/app/articles" size="sm" class="ml-auto gap-1">
View All
<ArrowUpRight class="h-4 w-4" />
</Button>
</Card.Header>
<Card.Content>
<Table.Root>
<Table.Header>
<Table.Row>
<Table.Head>Title</Table.Head>
<Table.Head class="xl:table.-column text-right">Date</Table.Head>
<Table.Head class="xl:table.-column text-right">Views</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#each data.dashboard_info.recentArticles as article}
<Table.Row>
<Table.Cell>
<div class="overflow-ellipsis font-medium">{article.title}</div>
</Table.Cell>
<Table.Cell class="text-right"
>{new Date(article.created_at).toLocaleDateString()}</Table.Cell
>
<Table.Cell class="text-right">{article.views}</Table.Cell>
</Table.Row>
{/each}
</Table.Body>
</Table.Root>
</Card.Content>
</Card.Root>
<Card.Root>
<Card.Header class="flex flex-row items-center">
<div class="grid gap-2">
<Card.Title>Recent Signups</Card.Title>
<!-- <Card.Description>Recent articles written from your youtube videos.</Card.Description> -->
</div>
<Button href="/app/emails" size="sm" class="ml-auto gap-1">
View All
<ArrowUpRight class="h-4 w-4" />
</Button>
</Card.Header>
<Card.Content class="grid gap-8">
{#each data.dashboard_info.recentSignups as signup}
<div class="flex items-center gap-4">
<!-- <Avatar.Root class="hidden h-9 w-9 sm:flex">
<Avatar.Image src="/avatars/01.png" alt="Avatar" />
<Avatar.Fallback>OM</Avatar.Fallback>
</Avatar.Root> -->
<div class="grid gap-1">
<!-- <p class="text-sm font-medium leading-none">Olivia Martin</p> -->
<p class="text-sm">{signup.email}</p>
</div>
<div class="ml-auto font-medium">{signup.created_at > 1000 * 60 * 60 * 24 * 7 ? new Date(signup.created_at).toLocaleDateString() : format(new Date(signup.created_at))}</div>
</div>
{/each}
</Card.Content>
</Card.Root>
</div>
<!-- <p>{JSON.stringify(data)}</p>
{#await me}
<p>loading...</p>
{:then me}
{#if me && channel}
<p>Welcome back!</p>
<h3>Your channel:</h3>
<p>{channel.title}</p>
{:else}
<a href="{config.api_url}/auth/google">Log in here you fat bastard</a>
{/if}
{/await} -->

View File

@ -1,23 +1,26 @@
import { config } from "$lib"
import { message, superValidate } from "sveltekit-superforms";
import { message, setError, superValidate, fail } from "sveltekit-superforms";
import { createFormSchema, editFormSchema } from "./schema";
import { zod } from "sveltekit-superforms/adapters";
import { fail } from "@sveltejs/kit";
/** @type {import("./$types").PageServerLoad} */
export const load = async ({ fetch }) => {
const blogRes = await fetch(config.api_url + "/blog?mine=true", {
export const load = async ({ fetch, url, request }) => {
const offset = (Number(url.searchParams.get("page") || 1) - 1) * 10;
// @ts-ignore
const blogRes = await fetch(config.api_url + "/blog?mine=true&offset=" + offset, {
credentials: 'include'
});
// const page = capPage(Number(url.searchParams.get("page")) || 1);
const videosRes = await fetch(config.api_url + "/videos", {
credentials: 'include'
});
const dataBlog = await blogRes.json();
const dataVideos = await videosRes.json();
return {
articles: dataBlog.articles,
total_articles: dataBlog.total_articles,
site: dataBlog.site,
videos: dataVideos.videos,
createForm: await superValidate(zod(createFormSchema)),
@ -35,7 +38,9 @@ export const actions = {
form,
});
}
const res = await event.fetch(config.api_url + "/blog/create", {
console.log(form.data);
const res = await event.fetch(config.api_url + "/dashboard/create", {
method: "POST",
body: JSON.stringify(form.data),
headers: {
@ -43,6 +48,13 @@ export const actions = {
}
});
const resData = await res.json();
if(!resData.success) {
// console.log(resData)
return setError(form, resData.message);
}
return {
form,
};
@ -55,7 +67,7 @@ export const actions = {
});
}
const res = await event.fetch(config.api_url + "/blog/article", {
const res = await event.fetch(config.api_url + "/dashboard/article", {
method: "PUT",
body: JSON.stringify(form.data),
headers: {
@ -75,4 +87,13 @@ export const actions = {
form
}
}
};
};
/**
*
* @param {number} pageN
* @returns {number}
*/
function capPage(pageN) {
return pageN > 0 ? pageN : 1
}

View File

@ -0,0 +1,165 @@
<script>
import * as Table from '$lib/components/ui/table/index.js';
import { ChevronRight, ExternalLink, Eye, EyeOff, Loader, Loader2, Pen, Trash } from 'lucide-svelte';
import TooltipButton from '$lib/components/molecules/tooltipbutton.svelte';
import CreateArticleDialog from './createArticleDialog.svelte';
import EditArticleDialog from './editArticleDialog.svelte';
import * as Pagination from '$lib/components/ui/pagination';
import { ChevronLeft } from 'svelte-radix';
import { onMount } from 'svelte';
import { browser } from '$app/environment';
/** @type {import("./$types").PageData} */
export let data;
/** @type {import("./schema.js").ArticleData | null} */
let editingContent = null;
/**
* @param {string} id
*/
function editArticle(id) {
fetch('/app/articles/getArticleBody?id=' + id)
.then((x) => x.json())
.then((data) => {
editingContent = data.article;
});
}
let currentPage = 1;
onMount(() => {
if(!browser) return;
let pageQuery = new URLSearchParams(window.location.search);
if(!pageQuery.has("page")) {
pageQuery.set("page", "1");
window.location.search = pageQuery.toString()
return;
}
currentPage = Number(pageQuery.get("page"));
})
$: {
if(data) {
isLoading = false;
}
}
$: currentPage
let isLoading = true;
</script>
<div class="mx-auto w-full max-w-[1000px] relative">
<CreateArticleDialog
form={data.createForm}
videos={data.videos}
tier={data.me.subscription_tier}
/>
{#if data.articles.length > 0}
{#if isLoading}
<div class="w-full h-full bg-[rgb(255 255 255 / 20%)] backdrop-blur-sm absolute z-40 flex justify-center items-center">
<Loader2 class="w-12 h-12 z-50 text-white animate-spin" />
</div>
{/if}
<Table.Root>
<Table.Header>
<Table.Row>
<Table.Head class="w-[25px]">Visibility</Table.Head>
<Table.Head class="max-w-[300px]">Title</Table.Head>
<Table.Head class="text-end">Source</Table.Head>
<Table.Head class="text-end">Views</Table.Head>
<Table.Head class="text-end">Actions</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#each data.articles as article, i (i)}
<Table.Row>
<Table.Cell class="font-medium">
{#if article.is_public}
<Eye class="mx-auto h-5 w-5" />
{:else}
<EyeOff class="mx-auto h-5 w-5" />
{/if}
</Table.Cell>
<Table.Cell class="max-w-[300px] overflow-hidden overflow-ellipsis text-nowrap"
>{article.id}</Table.Cell
>
<Table.Cell class="text-end">{'Youtube'}</Table.Cell>
<Table.Cell class="text-end">{article.views}</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"
on:click={() => editArticle(article.id)}
>
<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>
<Pagination.Root count={data.total_articles} perPage={10} let:pages currentPage={currentPage}>
<Pagination.Content>
<Pagination.Item>
<Pagination.PrevButton>
<ChevronLeft class="h-4 w-4" />
<span class="hidden sm:block">Previous</span>
</Pagination.PrevButton>
</Pagination.Item>
{#each pages as page (page.key)}
{#if page.type === 'ellipsis'}
<Pagination.Item>
<Pagination.Ellipsis />
</Pagination.Item>
{:else}
<Pagination.Item>
<Pagination.Link {page} isActive={currentPage === page.value} on:click={() => {
const pageQuery = new URLSearchParams(window.location.search);
if(pageQuery.get("page") == page.value.toString()) return;
console.log("fetching...")
pageQuery.set("page", page.value.toString())
window.location.search = pageQuery.toString();
isLoading = true
}}>
{page.value}
</Pagination.Link>
</Pagination.Item>
{/if}
{/each}
<Pagination.Item>
<Pagination.NextButton>
<span class="hidden sm:block">Next</span>
<ChevronRight class="h-4 w-4" />
</Pagination.NextButton>
</Pagination.Item>
</Pagination.Content>
</Pagination.Root>
{:else}
<p class="pt-12 text-center text-gray-400">
You don't have any articles yet. Start by making one!
</p>
{/if}
</div>
<EditArticleDialog bind:article_data={editingContent} form={data.editForm} />
<!-- <p>{JSON.stringify(data)}</p> -->

View File

@ -0,0 +1,175 @@
<script>
import ProBadge from '$lib/components/molecules/probadge.svelte';
import { Button, 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';
import { toast } from 'svelte-sonner';
import { superForm } from 'sveltekit-superforms';
import { zodClient } from 'sveltekit-superforms/adapters';
import { createFormSchema } from './schema';
import { page } from '$app/stores';
import { Field } from 'formsnap';
export let videos;
export let tier;
/** @type {import('sveltekit-superforms').SuperValidated<import('zod').infer<import('./schema').CreateFormSchema>, any>} */
let data = $page.data.switch;
export { data as form };
const form = superForm(data, {
validators: zodClient(createFormSchema)
});
const { form: formData, enhance, errors } = form;
let open = false;
function submitArticle() {
open = false;
if (!$errors._errors) toast('Article is queued for generation.');
}
errors.subscribe((x) => {
x._errors?.forEach((x) =>
toast.error(x, {
dismissable: true,
duration: 0
})
);
});
// message.subscribe((msg) => {
// if(msg) {
// toast.error(msg);
// }
// });
</script>
<Dialog.Root bind: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={submitArticle}
action="?/create"
>
<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 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]">
<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} {...attrs} />
</Select.Root>
</div>
</div>
</Form.Control>
<Form.Description />
<Form.FieldErrors />
</Form.Field>
<Form.Field {form} name="length">
<Form.Control let:attrs>
<div class="grid gap-4 py-4">
<div class="grid grid-cols-4 items-center gap-4">
<Form.Label 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)"
disabled={['free', 'basic'].includes(tier)}
>Medium (~1500 words)<ProBadge /></Select.Item
>
<Select.Item
value="2500"
label="Long (~2500 words)"
disabled={['free', 'basic'].includes(tier)}
>Long (~2500 words)<ProBadge /></Select.Item
>
</Select.Group>
</Select.Content>
<Select.Input {...attrs} />
</Select.Root>
</div>
</div>
</Form.Control>
</Form.Field>
<Form.Field {form} name="format">
<Form.Control let:attrs>
<div class="grid gap-4 py-4">
<div class="grid grid-cols-4 items-center gap-4">
<Form.Label 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.Field>
<Form.Field {form} name="faq">
<Form.Control let:attrs>
<div class="grid grid-cols-4 items-center gap-4">
<Form.Label class="text-right">Include FAQ</Form.Label>
<div class="flex items-center justify-start">
<Switch disabled={['free', 'basic'].includes(tier)} {...attrs} />
<ProBadge class="ml-[10px]" />
</div>
</div>
</Form.Control>
</Form.Field>
</form>
<Dialog.Footer>
<Button type="submit" form="blog-converter">Create</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>

View File

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

View File

@ -1,30 +1,59 @@
<script>
import * as Table from '$lib/components/ui/table/index.js';
import { ExternalLink, Pen, Trash } from 'lucide-svelte';
import { ArrowUpRight, ExternalLink, Pen, Trash } from 'lucide-svelte';
import TooltipButton from '$lib/components/molecules/tooltipbutton.svelte';
import { Button } from '$lib/components/ui/button';
/** @type {import("./$types").PageData} */
export let data;
async function exportAsCsv() {
const response = await fetch('/app/emails/export');
const csvData = await response.text();
// Create a blob from the CSV data
const blob = new Blob([csvData], { type: 'text/csv' });
// Create a temporary URL for the blob
const url = window.URL.createObjectURL(blob);
// Create a link element to trigger the download
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', 'data.csv');
// Simulate a click on the link to trigger the download
document.body.appendChild(link);
link.click();
// Clean up
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
}
</script>
<div class="mx-auto w-full max-w-[1000px]">
<Button on:click={exportAsCsv} size="sm" class="ml-auto gap-1">
Export as CSV
<ArrowUpRight class="h-4 w-4" />
</Button>
<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">Date and Time</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.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="text-end">{new Date(signup.created_at).toLocaleString()}</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" />
@ -37,9 +66,9 @@
</TooltipButton>
</Table.Cell> -->
</Table.Row>
{/each}
</Table.Body>
</Table.Root>
{/each}
</Table.Body>
</Table.Root>
</div>
<!-- <p>{JSON.stringify(data)}</p> -->

View File

@ -0,0 +1,8 @@
import { config } from '$lib';
import { error, json, text } from '@sveltejs/kit';
/** @type {import('./$types').RequestHandler} */
export async function GET(event) {
const exportedCsv = await event.fetch(config.api_url + "/dashboard/signups/export").then(x=>x.json());
return new Response(exportedCsv.data);
}

View File

@ -0,0 +1,54 @@
import { config } from "$lib";
import { fail, message, superValidate } from "sveltekit-superforms";
import { schema } from "./schema";
import { zod } from "sveltekit-superforms/adapters";
/** @type {import("./$types").PageServerLoad} */
export const load = async ({ fetch }) => {
const blogRes = await fetch(config.api_url + "/blog?mine=true", {
credentials: 'include'
});
const {site} = await blogRes.json();
site.primary_color_hex = "#" + site.primary_color_hex
site.secondary_color_hex = "#" + site.secondary_color_hex
site.text_color_hex = "#" + site.text_color_hex
return {
blog_info: site,
form: await superValidate(site, zod(schema))
}
}
/** @type {import("@sveltejs/kit").Actions} */
export const actions = {
default: async (event) => {
const form = await superValidate(event, zod(schema));
if (!form.valid) {
return fail(400, {
form,
});
}
let data = structuredClone(form.data);
data.primary_color_hex = data.primary_color_hex.slice(1);
data.secondary_color_hex = data.secondary_color_hex.slice(1);
data.text_color_hex = data.text_color_hex.slice(1);
const res = await event.fetch(config.api_url + "/dashboard/website", {
method: "PUT",
body: JSON.stringify(data),
headers: {
"content-type": "application/json"
}
});
const res_data = await res.json();
if(!res_data.success) return fail(res.status, {
form
});
return {
form,
};
},
}

View File

@ -0,0 +1,77 @@
<script>
import * as Form from '$lib/components/ui/form';
import { Input } from '$lib/components/ui/input';
import { superForm } from 'sveltekit-superforms';
import { zodClient } from 'sveltekit-superforms/adapters';
import { schema } from './schema';
import ColorPicker from 'svelte-awesome-color-picker';
import { toast } from 'svelte-sonner';
/** @type {import("./$types").PageServerData} */
export let data;
const form = superForm(data.form, {
validators: zodClient(schema),
resetForm: false,
onResult: (e) => {
if(e.result.type == "success") {
toast.success("Website configuration successfully updated!");
}
else {
toast.error("Error when updating website.");
}
}
});
const { form: formData, enhance } = form;
$formData.id = data.blog_info.id
console.log(data.blog_info)
$: {
console.log($formData.primary_color_hex)
}
</script>
<div class="max-w-2xl w-full">
<form method="post" use:enhance>
<input type="text" value="{data.blog_info.id}" name="id" readonly class="hidden">
<Form.Field {form} name="name">
<Form.Control let:attrs>
<Form.Label>Blog name</Form.Label>
<Input {...attrs} bind:value={$formData.name} />
</Form.Control>
<Form.Description />
<Form.FieldErrors />
</Form.Field>
<Form.Field {form} name="primary_color_hex">
<Form.Control let:attrs>
<Form.Label>Primary color</Form.Label>
<ColorPicker on:input={e=> {
if(e.detail.hex) $formData.primary_color_hex = e.detail.hex
}} hex={data.blog_info.primary_color_hex} {...attrs}/>
</Form.Control>
<Form.Description />
<Form.FieldErrors />
</Form.Field>
<Form.Field {form} name="secondary_color_hex">
<Form.Control let:attrs>
<Form.Label>Secondary color</Form.Label>
<ColorPicker on:input={e=> {
if(e.detail.hex) $formData.secondary_color_hex = e.detail.hex
}} hex={data.blog_info.secondary_color_hex} {...attrs}/>
</Form.Control>
<Form.Description />
<Form.FieldErrors />
</Form.Field>
<Form.Field {form} name="text_color_hex">
<Form.Control let:attrs>
<Form.Label>Text color</Form.Label>
<ColorPicker on:input={e=> {
if(e.detail.hex) $formData.text_color_hex = e.detail.hex
}} hex={data.blog_info.text_color_hex} {...attrs}/>
</Form.Control>
<Form.Description />
<Form.FieldErrors />
</Form.Field>
<Form.Button>Submit</Form.Button>
</form>
</div>

View File

@ -0,0 +1,9 @@
import { z } from "zod";
export const schema = z.object({
id: z.string(),
name: z.string().min(4).max(16),
primary_color_hex: z.string().length(7),
secondary_color_hex: z.string().length(7),
text_color_hex: z.string().length(7),
});

View File

@ -0,0 +1,26 @@
<script>
import '../../../../app.pcss';
import { config } from '$lib';
import { Button } from '$lib/components/ui/button';
import { ModeWatcher } from 'mode-watcher';
import { IconBrandGoogle } from '@tabler/icons-svelte';
</script>
<ModeWatcher />
<div class="mx-auto flex h-full w-full max-w-64 items-center justify-center">
<!-- <Button href="{config.api_url}/auth/google"><GoogleIcon class="h-4 w-4 mr-2" /> Log in with google</Button> -->
<a
href="{config.api_url}/auth/google"
class="group/btn relative flex h-10 w-full items-center justify-start space-x-2 rounded-md bg-gray-50 px-4 font-medium text-black shadow-input dark:bg-zinc-900 dark:shadow-[0px_0px_1px_1px_var(--neutral-800)]"
>
<IconBrandGoogle class="h-4 w-4 text-neutral-800 dark:text-neutral-300" />
<span class="text-sm text-neutral-700 dark:text-neutral-300"> Google </span>
<span
class="absolute inset-x-0 -bottom-px block h-px w-full bg-gradient-to-r from-transparent via-cyan-500 to-transparent opacity-0 transition duration-500 group-hover/btn:opacity-100"
/>
<span
class="absolute inset-x-10 -bottom-px mx-auto block h-px w-1/2 bg-gradient-to-r from-transparent via-indigo-500 to-transparent opacity-0 blur-sm transition duration-500 group-hover/btn:opacity-100"
/>
</a>
</div>

View File

@ -0,0 +1,8 @@
/** @type {import('./$types').RequestHandler} */
export function GET(event) {
event.cookies.delete("token", {
path: "/"
});
return new Response("success");
}

View File

@ -0,0 +1,20 @@
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").LayoutServerLoad} */
export async function load(event) {
const data = await event.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,
root: event.url.origin + "/site/" + event.params.site_id
}
}

View File

@ -0,0 +1,49 @@
<script>
import { browser } from '$app/environment';
import { enhance } from '$app/forms';
import '../../../../app.pcss';
let currentPath = '';
$: currentPath = (browser && window.location.pathname) || '/';
/** @type {import('./$types').LayoutServerData} */
export let data;
let is_subscribed = false;
</script>
<div class="mx-auto max-w-screen-xl" style="color: #{data?.site.text_color_hex} !important">
<div class="p-4 shadow-lg rounded-b-lg">
<a href={data.root}>{data?.site.name}</a>
</div>
<slot />
<div>
{#if is_subscribed}
<span>Thank you for subscribing to our newsletter!</span>
{:else}
<form
method="post"
action="{data.root}/?"
use:enhance
on:submit={() => {
is_subscribed = true;
}}
>
<input
type="text"
style="visibility: hidden;"
name="source"
value={typeof window !== 'undefined' ? window.location.href : ''}
/>
<h4>sign up to our newsletter!</h4>
<input type="email" name="email" class="border-2 border-solid border-gray-500" placeholder="Your email" />
<button type="submit" class="py-1 px-4 rounded-md" style="background-color: #{data?.site.secondary_color_hex}">Sign up!</button>
</form>
{/if}
</div>
</div>

View File

@ -6,18 +6,6 @@ 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) => {
@ -31,7 +19,8 @@ export const actions = {
method: "POST",
body: JSON.stringify({
email: form.data.email,
site_id: event.params.site_id
site_id: event.params.site_id,
source: extractSlug(form.data.source)
}),
headers: {
"content-type": "application/json"
@ -42,4 +31,13 @@ export const actions = {
form,
};
},
};
};
/**
* @param {string | URL} url
*/
function extractSlug(url) {
const path = new URL(url).pathname;
const parts = path.split('/').filter(x=>x);
return parts[3]
}

View File

@ -0,0 +1,19 @@
<script>
import { browser } from '$app/environment';
let currentPath = '';
$: currentPath = (browser && window.location.pathname) || '/';
export let data;
</script>
<div class="grid grid-cols-1">
{#each data.articles as article}
<div class="p-4">
<a href={currentPath + '/' + article.seo_slug}>
<p class="m-0">{article.title}</p>
<p class="border-b-2 pb-2" style="border-color: #{data.site.secondary_color_hex}">{new Date(article.created_at).toLocaleString("de")}</p>
</a>
</div>
{/each}
</div>

View File

@ -1,8 +1,9 @@
<script>
import "./md-style.css"
import SvelteMarkdown from 'svelte-markdown'
export let data;
</script>
<p></p>
<h1>
{data.article.title}
</h1>

View File

@ -0,0 +1,9 @@
h1 {
font-size: 1.8rem;
font-weight: 700;
margin-bottom: 1rem;
}
p {
margin-bottom: 1rem
}

View File

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

View File

@ -1,27 +0,0 @@
<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

@ -1,10 +1,14 @@
import { fontFamily } from "tailwindcss/defaultTheme";
import flattenColorPalette from 'tailwindcss/lib/util/flattenColorPalette';
/** @type {import('tailwindcss').Config} */
const config = {
darkMode: ["class"],
content: ["./src/**/*.{html,js,svelte,ts}"],
safelist: ["dark"],
plugins: [
addVariablesForColors
],
theme: {
container: {
center: true,
@ -61,4 +65,15 @@ const config = {
},
};
function addVariablesForColors({ addBase, theme }) {
let allColors = flattenColorPalette(theme('colors'));
let newVars = Object.fromEntries(
Object.entries(allColors).map(([key, val]) => [`--${key}`, val])
);
addBase({
':root': newVars
});
}
export default config;