whoaaaaa lots of stuff changed
This commit is contained in:
parent
3f8ba248cc
commit
193113e029
186
package-lock.json
generated
186
package-lock.json
generated
@ -14,6 +14,7 @@
|
|||||||
"lucide-svelte": "^0.373.0",
|
"lucide-svelte": "^0.373.0",
|
||||||
"mode-watcher": "^0.3.0",
|
"mode-watcher": "^0.3.0",
|
||||||
"svelte-markdown": "^0.4.1",
|
"svelte-markdown": "^0.4.1",
|
||||||
|
"svelte-motion": "^0.12.2",
|
||||||
"svelte-radix": "^1.1.0",
|
"svelte-radix": "^1.1.0",
|
||||||
"svelte-sonner": "^0.3.22",
|
"svelte-sonner": "^0.3.22",
|
||||||
"sveltekit-superforms": "^2.12.6",
|
"sveltekit-superforms": "^2.12.6",
|
||||||
@ -25,6 +26,7 @@
|
|||||||
"@sveltejs/adapter-auto": "^3.0.0",
|
"@sveltejs/adapter-auto": "^3.0.0",
|
||||||
"@sveltejs/kit": "^2.0.0",
|
"@sveltejs/kit": "^2.0.0",
|
||||||
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
||||||
|
"@tabler/icons-svelte": "^3.3.0",
|
||||||
"autoprefixer": "^10.4.16",
|
"autoprefixer": "^10.4.16",
|
||||||
"postcss": "^8.4.32",
|
"postcss": "^8.4.32",
|
||||||
"postcss-load-config": "^5.0.2",
|
"postcss-load-config": "^5.0.2",
|
||||||
@ -32,8 +34,11 @@
|
|||||||
"prettier-plugin-svelte": "^3.1.2",
|
"prettier-plugin-svelte": "^3.1.2",
|
||||||
"prettier-plugin-tailwindcss": "^0.5.9",
|
"prettier-plugin-tailwindcss": "^0.5.9",
|
||||||
"svelte": "^4.2.7",
|
"svelte": "^4.2.7",
|
||||||
|
"svelte-awesome-color-picker": "^3.0.6",
|
||||||
"svelte-check": "^3.6.0",
|
"svelte-check": "^3.6.0",
|
||||||
|
"svelte-legos": "^0.2.2",
|
||||||
"tailwindcss": "^3.3.6",
|
"tailwindcss": "^3.3.6",
|
||||||
|
"timeago.js": "^4.0.2",
|
||||||
"typescript": "^5.0.0",
|
"typescript": "^5.0.0",
|
||||||
"vite": "^5.0.3"
|
"vite": "^5.0.3"
|
||||||
}
|
}
|
||||||
@ -937,6 +942,32 @@
|
|||||||
"tslib": "^2.4.0"
|
"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": {
|
"node_modules/@types/cookie": {
|
||||||
"version": "0.6.0",
|
"version": "0.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/@types/marked/-/marked-5.0.2.tgz",
|
||||||
"integrity": "sha512-OucS4KMHhFzhz27KxmWg7J+kIYqyqoW5kdIEI319hqARQQUTqhao3M/F+uFnDXD0Rg72iDDZxZNxq5gvctmLlg=="
|
"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": {
|
"node_modules/@types/pug": {
|
||||||
"version": "2.0.10",
|
"version": "2.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.10.tgz",
|
||||||
"integrity": "sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==",
|
"integrity": "sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/@types/validator": {
|
||||||
"version": "13.11.9",
|
"version": "13.11.9",
|
||||||
"resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.9.tgz",
|
"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": {
|
"node_modules/chokidar": {
|
||||||
"version": "3.6.0",
|
"version": "3.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
"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": {
|
"node_modules/commander": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
|
||||||
@ -1397,6 +1458,11 @@
|
|||||||
"node": ">=4"
|
"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": {
|
"node_modules/dayjs": {
|
||||||
"version": "1.11.10",
|
"version": "1.11.10",
|
||||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz",
|
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz",
|
||||||
@ -1663,6 +1729,19 @@
|
|||||||
"url": "https://github.com/sponsors/rawify"
|
"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": {
|
"node_modules/fs.realpath": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||||
@ -1748,6 +1827,11 @@
|
|||||||
"node": ">= 0.4"
|
"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": {
|
"node_modules/import-fresh": {
|
||||||
"version": "3.3.0",
|
"version": "3.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
|
||||||
@ -2254,6 +2338,22 @@
|
|||||||
"node": ">= 6"
|
"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": {
|
"node_modules/postcss": {
|
||||||
"version": "8.4.38",
|
"version": "8.4.38",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz",
|
"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": {
|
"node_modules/property-expr": {
|
||||||
"version": "2.0.6",
|
"version": "2.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz",
|
||||||
@ -2850,6 +2965,20 @@
|
|||||||
"node": ">=8"
|
"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": {
|
"node_modules/sucrase": {
|
||||||
"version": "3.35.0",
|
"version": "3.35.0",
|
||||||
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
|
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
|
||||||
@ -2958,6 +3087,28 @@
|
|||||||
"node": ">=16"
|
"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": {
|
"node_modules/svelte-check": {
|
||||||
"version": "3.6.9",
|
"version": "3.6.9",
|
||||||
"resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-3.6.9.tgz",
|
"resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-3.6.9.tgz",
|
||||||
@ -2991,6 +3142,20 @@
|
|||||||
"svelte": "^3.19.0 || ^4.0.0"
|
"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": {
|
"node_modules/svelte-markdown": {
|
||||||
"version": "0.4.1",
|
"version": "0.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/svelte-markdown/-/svelte-markdown-0.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/svelte-markdown/-/svelte-markdown-0.4.1.tgz",
|
||||||
@ -3003,6 +3168,21 @@
|
|||||||
"svelte": "^4.0.0"
|
"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": {
|
"node_modules/svelte-preprocess": {
|
||||||
"version": "5.1.4",
|
"version": "5.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-5.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-5.1.4.tgz",
|
||||||
@ -3313,6 +3493,12 @@
|
|||||||
"node": ">=0.8"
|
"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": {
|
"node_modules/tiny-case": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz",
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
"@sveltejs/adapter-auto": "^3.0.0",
|
"@sveltejs/adapter-auto": "^3.0.0",
|
||||||
"@sveltejs/kit": "^2.0.0",
|
"@sveltejs/kit": "^2.0.0",
|
||||||
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
||||||
|
"@tabler/icons-svelte": "^3.3.0",
|
||||||
"autoprefixer": "^10.4.16",
|
"autoprefixer": "^10.4.16",
|
||||||
"postcss": "^8.4.32",
|
"postcss": "^8.4.32",
|
||||||
"postcss-load-config": "^5.0.2",
|
"postcss-load-config": "^5.0.2",
|
||||||
@ -22,8 +23,11 @@
|
|||||||
"prettier-plugin-svelte": "^3.1.2",
|
"prettier-plugin-svelte": "^3.1.2",
|
||||||
"prettier-plugin-tailwindcss": "^0.5.9",
|
"prettier-plugin-tailwindcss": "^0.5.9",
|
||||||
"svelte": "^4.2.7",
|
"svelte": "^4.2.7",
|
||||||
|
"svelte-awesome-color-picker": "^3.0.6",
|
||||||
"svelte-check": "^3.6.0",
|
"svelte-check": "^3.6.0",
|
||||||
|
"svelte-legos": "^0.2.2",
|
||||||
"tailwindcss": "^3.3.6",
|
"tailwindcss": "^3.3.6",
|
||||||
|
"timeago.js": "^4.0.2",
|
||||||
"typescript": "^5.0.0",
|
"typescript": "^5.0.0",
|
||||||
"vite": "^5.0.3"
|
"vite": "^5.0.3"
|
||||||
},
|
},
|
||||||
@ -35,6 +39,7 @@
|
|||||||
"lucide-svelte": "^0.373.0",
|
"lucide-svelte": "^0.373.0",
|
||||||
"mode-watcher": "^0.3.0",
|
"mode-watcher": "^0.3.0",
|
||||||
"svelte-markdown": "^0.4.1",
|
"svelte-markdown": "^0.4.1",
|
||||||
|
"svelte-motion": "^0.12.2",
|
||||||
"svelte-radix": "^1.1.0",
|
"svelte-radix": "^1.1.0",
|
||||||
"svelte-sonner": "^0.3.22",
|
"svelte-sonner": "^0.3.22",
|
||||||
"sveltekit-superforms": "^2.12.6",
|
"sveltekit-superforms": "^2.12.6",
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
%sveltekit.head%
|
%sveltekit.head%
|
||||||
</head>
|
</head>
|
||||||
<body data-sveltekit-preload-data="hover">
|
<body data-sveltekit-preload-data="hover" style="height: 100vh">
|
||||||
<div style="display: contents">%sveltekit.body%</div>
|
<div style="display: contents">%sveltekit.body%</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
116
src/lib/components/molecules/BackgroundBeams.svelte
Normal file
116
src/lib/components/molecules/BackgroundBeams.svelte
Normal 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>
|
24
src/lib/components/ui/pagination/index.js
Normal file
24
src/lib/components/ui/pagination/index.js
Normal 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,
|
||||||
|
};
|
@ -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>
|
15
src/lib/components/ui/pagination/pagination-ellipsis.svelte
Normal file
15
src/lib/components/ui/pagination/pagination-ellipsis.svelte
Normal 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>
|
9
src/lib/components/ui/pagination/pagination-item.svelte
Normal file
9
src/lib/components/ui/pagination/pagination-item.svelte
Normal 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>
|
25
src/lib/components/ui/pagination/pagination-link.svelte
Normal file
25
src/lib/components/ui/pagination/pagination-link.svelte
Normal 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>
|
@ -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>
|
@ -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>
|
27
src/lib/components/ui/pagination/pagination.svelte
Normal file
27
src/lib/components/ui/pagination/pagination.svelte
Normal 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>
|
@ -2,6 +2,11 @@ import { clsx } from "clsx";
|
|||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
import { cubicOut } from "svelte/easing";
|
import { cubicOut } from "svelte/easing";
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {import("clsx").ClassValue[]} inputs
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
export function cn(...inputs) {
|
export function cn(...inputs) {
|
||||||
return twMerge(clsx(inputs));
|
return twMerge(clsx(inputs));
|
||||||
}
|
}
|
||||||
|
@ -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} -->
|
|
@ -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> -->
|
|
@ -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>
|
|
@ -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
39
src/routes/+page.svelte
Normal 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>
|
@ -3,7 +3,10 @@ import { redirect } from "@sveltejs/kit";
|
|||||||
|
|
||||||
/** @type {import("./$types").LayoutServerLoad} */
|
/** @type {import("./$types").LayoutServerLoad} */
|
||||||
export const load = async ({fetch, cookies}) => {
|
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")
|
const res = await fetch(config.api_url+"/me")
|
||||||
if(res.status > 399 && res.status < 499) {
|
if(res.status > 399 && res.status < 499) {
|
||||||
cookies.delete("token", {
|
cookies.delete("token", {
|
||||||
@ -15,6 +18,6 @@ export const load = async ({fetch, cookies}) => {
|
|||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
me: data
|
me: data.user
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,5 +1,5 @@
|
|||||||
<script>
|
<script>
|
||||||
import '../../app.pcss';
|
import '../../../app.pcss';
|
||||||
|
|
||||||
import Menu from 'lucide-svelte/icons/menu';
|
import Menu from 'lucide-svelte/icons/menu';
|
||||||
import Package2 from 'lucide-svelte/icons/package-2';
|
import Package2 from 'lucide-svelte/icons/package-2';
|
||||||
@ -20,12 +20,17 @@
|
|||||||
export let data;
|
export let data;
|
||||||
|
|
||||||
const navs = [
|
const navs = [
|
||||||
{ name: 'Dashboard', redirect: '/' },
|
{ name: 'Dashboard', redirect: '/app' },
|
||||||
{ name: 'Articles', redirect: '/articles' },
|
{ name: 'Articles', redirect: '/app/articles' },
|
||||||
{ name: 'Emails', redirect: '/emails' },
|
{ name: 'Emails', redirect: '/app/emails' },
|
||||||
{ name: 'Website', redirect: '##' },
|
{ name: 'Website', redirect: '/app/website' },
|
||||||
{ name: 'Analytics', redirect: '##' },
|
{ name: 'Analytics', redirect: '##' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
async function logout() {
|
||||||
|
fetch("/app/auth/logout");
|
||||||
|
window.location.href = "/"
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Toaster />
|
<Toaster />
|
||||||
@ -38,7 +43,7 @@
|
|||||||
>
|
>
|
||||||
<a href="##" class="flex items-center gap-2 text-lg font-semibold md:text-base">
|
<a href="##" class="flex items-center gap-2 text-lg font-semibold md:text-base">
|
||||||
<Package2 class="h-6 w-6" />
|
<Package2 class="h-6 w-6" />
|
||||||
<span class="sr-only">{data.me.user.name}</span>
|
<span class="sr-only">{data.me.name}</span>
|
||||||
</a>
|
</a>
|
||||||
{#each navs as nav}
|
{#each navs as nav}
|
||||||
<a href="{nav.redirect}" class="text-muted-foreground transition-colors hover:text-foreground">{nav.name}</a>
|
<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>Settings</DropdownMenu.Item>
|
||||||
<DropdownMenu.Item>Support</DropdownMenu.Item>
|
<DropdownMenu.Item>Support</DropdownMenu.Item>
|
||||||
<DropdownMenu.Separator />
|
<DropdownMenu.Separator />
|
||||||
<DropdownMenu.Item>Logout</DropdownMenu.Item>
|
<DropdownMenu.Item on:click={logout}>Logout</DropdownMenu.Item>
|
||||||
</DropdownMenu.Content>
|
</DropdownMenu.Content>
|
||||||
</DropdownMenu.Root>
|
</DropdownMenu.Root>
|
||||||
</div>
|
</div>
|
19
src/routes/app/(app)/+page.server.js
Normal file
19
src/routes/app/(app)/+page.server.js
Normal 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
|
||||||
|
}
|
||||||
|
}
|
137
src/routes/app/(app)/+page.svelte
Normal file
137
src/routes/app/(app)/+page.svelte
Normal 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} -->
|
@ -1,23 +1,26 @@
|
|||||||
import { config } from "$lib"
|
import { config } from "$lib"
|
||||||
import { message, superValidate } from "sveltekit-superforms";
|
import { message, setError, superValidate, fail } from "sveltekit-superforms";
|
||||||
import { createFormSchema, editFormSchema } from "./schema";
|
import { createFormSchema, editFormSchema } from "./schema";
|
||||||
import { zod } from "sveltekit-superforms/adapters";
|
import { zod } from "sveltekit-superforms/adapters";
|
||||||
import { fail } from "@sveltejs/kit";
|
|
||||||
|
|
||||||
/** @type {import("./$types").PageServerLoad} */
|
/** @type {import("./$types").PageServerLoad} */
|
||||||
export const load = async ({ fetch }) => {
|
export const load = async ({ fetch, url, request }) => {
|
||||||
const blogRes = await fetch(config.api_url + "/blog?mine=true", {
|
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'
|
credentials: 'include'
|
||||||
});
|
});
|
||||||
|
// const page = capPage(Number(url.searchParams.get("page")) || 1);
|
||||||
|
|
||||||
const videosRes = await fetch(config.api_url + "/videos", {
|
const videosRes = await fetch(config.api_url + "/videos", {
|
||||||
credentials: 'include'
|
credentials: 'include'
|
||||||
});
|
});
|
||||||
|
|
||||||
const dataBlog = await blogRes.json();
|
const dataBlog = await blogRes.json();
|
||||||
const dataVideos = await videosRes.json();
|
const dataVideos = await videosRes.json();
|
||||||
return {
|
return {
|
||||||
articles: dataBlog.articles,
|
articles: dataBlog.articles,
|
||||||
|
total_articles: dataBlog.total_articles,
|
||||||
site: dataBlog.site,
|
site: dataBlog.site,
|
||||||
videos: dataVideos.videos,
|
videos: dataVideos.videos,
|
||||||
createForm: await superValidate(zod(createFormSchema)),
|
createForm: await superValidate(zod(createFormSchema)),
|
||||||
@ -35,7 +38,9 @@ export const actions = {
|
|||||||
form,
|
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",
|
method: "POST",
|
||||||
body: JSON.stringify(form.data),
|
body: JSON.stringify(form.data),
|
||||||
headers: {
|
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 {
|
return {
|
||||||
form,
|
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",
|
method: "PUT",
|
||||||
body: JSON.stringify(form.data),
|
body: JSON.stringify(form.data),
|
||||||
headers: {
|
headers: {
|
||||||
@ -75,4 +87,13 @@ export const actions = {
|
|||||||
form
|
form
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {number} pageN
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
function capPage(pageN) {
|
||||||
|
return pageN > 0 ? pageN : 1
|
||||||
|
}
|
165
src/routes/app/(app)/articles/+page.svelte
Normal file
165
src/routes/app/(app)/articles/+page.svelte
Normal 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> -->
|
175
src/routes/app/(app)/articles/createArticleDialog.svelte
Normal file
175
src/routes/app/(app)/articles/createArticleDialog.svelte
Normal 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>
|
@ -2,7 +2,7 @@ import { config } from "$lib";
|
|||||||
|
|
||||||
/** @type {import("./$types").PageServerLoad} */
|
/** @type {import("./$types").PageServerLoad} */
|
||||||
export const load = async ({ fetch }) => {
|
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'
|
credentials: 'include'
|
||||||
});
|
});
|
||||||
|
|
@ -1,30 +1,59 @@
|
|||||||
<script>
|
<script>
|
||||||
import * as Table from '$lib/components/ui/table/index.js';
|
import * as Table from '$lib/components/ui/table/index.js';
|
||||||
import { ExternalLink, Pen, Trash } from 'lucide-svelte';
|
import { ArrowUpRight, ExternalLink, Pen, Trash } from 'lucide-svelte';
|
||||||
|
|
||||||
import TooltipButton from '$lib/components/molecules/tooltipbutton.svelte';
|
import TooltipButton from '$lib/components/molecules/tooltipbutton.svelte';
|
||||||
|
import { Button } from '$lib/components/ui/button';
|
||||||
|
|
||||||
/** @type {import("./$types").PageData} */
|
/** @type {import("./$types").PageData} */
|
||||||
export let data;
|
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>
|
</script>
|
||||||
|
|
||||||
<div class="mx-auto w-full max-w-[1000px]">
|
<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.Root>
|
||||||
<Table.Caption>A list of recent signups on your website.</Table.Caption>
|
<Table.Caption>A list of recent signups on your website.</Table.Caption>
|
||||||
<Table.Header>
|
<Table.Header>
|
||||||
<Table.Row>
|
<Table.Row>
|
||||||
<Table.Head class="max-w-[300px]">Email</Table.Head>
|
<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.Head class="text-end">Actions</Table.Head>
|
||||||
</Table.Row>
|
</Table.Row>
|
||||||
</Table.Header>
|
</Table.Header>
|
||||||
<Table.Body>
|
<Table.Body>
|
||||||
{#each data.signups as signup, i (i)}
|
{#each data.signups as signup, i (i)}
|
||||||
<Table.Row>
|
<Table.Row>
|
||||||
<Table.Cell class="max-w-[300px] overflow-hidden overflow-ellipsis text-nowrap"
|
<Table.Cell class="max-w-[300px] overflow-hidden overflow-ellipsis text-nowrap"
|
||||||
>{signup.email}</Table.Cell
|
>{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">
|
<!-- <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)}>
|
<TooltipButton class="hover:bg-blue-600" variant="outline" size="icon" tip="Preview" on:click={() => window.open("/site/" + data.site.id + "/" + article.seo_slug)}>
|
||||||
<ExternalLink size="1rem" />
|
<ExternalLink size="1rem" />
|
||||||
@ -37,9 +66,9 @@
|
|||||||
</TooltipButton>
|
</TooltipButton>
|
||||||
</Table.Cell> -->
|
</Table.Cell> -->
|
||||||
</Table.Row>
|
</Table.Row>
|
||||||
{/each}
|
{/each}
|
||||||
</Table.Body>
|
</Table.Body>
|
||||||
</Table.Root>
|
</Table.Root>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- <p>{JSON.stringify(data)}</p> -->
|
<!-- <p>{JSON.stringify(data)}</p> -->
|
8
src/routes/app/(app)/emails/export/+server.js
Normal file
8
src/routes/app/(app)/emails/export/+server.js
Normal 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);
|
||||||
|
}
|
54
src/routes/app/(app)/website/+page.server.js
Normal file
54
src/routes/app/(app)/website/+page.server.js
Normal 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,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}
|
77
src/routes/app/(app)/website/+page.svelte
Normal file
77
src/routes/app/(app)/website/+page.svelte
Normal 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>
|
9
src/routes/app/(app)/website/schema.js
Normal file
9
src/routes/app/(app)/website/schema.js
Normal 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),
|
||||||
|
});
|
26
src/routes/app/(auth)/auth/+page.svelte
Normal file
26
src/routes/app/(auth)/auth/+page.svelte
Normal 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>
|
8
src/routes/app/(auth)/auth/logout/+server.js
Normal file
8
src/routes/app/(auth)/auth/logout/+server.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
/** @type {import('./$types').RequestHandler} */
|
||||||
|
export function GET(event) {
|
||||||
|
event.cookies.delete("token", {
|
||||||
|
path: "/"
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Response("success");
|
||||||
|
}
|
20
src/routes/app/site/[site_id]/+layout.server.js
Normal file
20
src/routes/app/site/[site_id]/+layout.server.js
Normal 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
|
||||||
|
}
|
||||||
|
}
|
49
src/routes/app/site/[site_id]/+layout.svelte
Normal file
49
src/routes/app/site/[site_id]/+layout.svelte
Normal 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>
|
@ -6,18 +6,6 @@ import { zod } from "sveltekit-superforms/adapters";
|
|||||||
import { formSchema } from "./schema";
|
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} */
|
/** @type {import("@sveltejs/kit").Actions} */
|
||||||
export const actions = {
|
export const actions = {
|
||||||
default: async (event) => {
|
default: async (event) => {
|
||||||
@ -31,7 +19,8 @@ export const actions = {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
email: form.data.email,
|
email: form.data.email,
|
||||||
site_id: event.params.site_id
|
site_id: event.params.site_id,
|
||||||
|
source: extractSlug(form.data.source)
|
||||||
}),
|
}),
|
||||||
headers: {
|
headers: {
|
||||||
"content-type": "application/json"
|
"content-type": "application/json"
|
||||||
@ -42,4 +31,13 @@ export const actions = {
|
|||||||
form,
|
form,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string | URL} url
|
||||||
|
*/
|
||||||
|
function extractSlug(url) {
|
||||||
|
const path = new URL(url).pathname;
|
||||||
|
const parts = path.split('/').filter(x=>x);
|
||||||
|
return parts[3]
|
||||||
|
}
|
19
src/routes/app/site/[site_id]/+page.svelte
Normal file
19
src/routes/app/site/[site_id]/+page.svelte
Normal 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>
|
@ -1,8 +1,9 @@
|
|||||||
<script>
|
<script>
|
||||||
|
import "./md-style.css"
|
||||||
import SvelteMarkdown from 'svelte-markdown'
|
import SvelteMarkdown from 'svelte-markdown'
|
||||||
export let data;
|
export let data;
|
||||||
</script>
|
</script>
|
||||||
|
<p></p>
|
||||||
<h1>
|
<h1>
|
||||||
{data.article.title}
|
{data.article.title}
|
||||||
</h1>
|
</h1>
|
9
src/routes/app/site/[site_id]/[article_id]/md-style.css
Normal file
9
src/routes/app/site/[site_id]/[article_id]/md-style.css
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
h1 {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-bottom: 1rem
|
||||||
|
}
|
@ -2,6 +2,7 @@ import { z } from "zod";
|
|||||||
|
|
||||||
export const formSchema = z.object({
|
export const formSchema = z.object({
|
||||||
email: z.string().email().min(1),
|
email: z.string().email().min(1),
|
||||||
|
source: z.string().url()
|
||||||
});
|
});
|
||||||
|
|
||||||
/** @typedef {typeof formSchema} FormSchema */
|
/** @typedef {typeof formSchema} FormSchema */
|
@ -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>
|
|
@ -1,10 +1,14 @@
|
|||||||
import { fontFamily } from "tailwindcss/defaultTheme";
|
import { fontFamily } from "tailwindcss/defaultTheme";
|
||||||
|
import flattenColorPalette from 'tailwindcss/lib/util/flattenColorPalette';
|
||||||
|
|
||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
const config = {
|
const config = {
|
||||||
darkMode: ["class"],
|
darkMode: ["class"],
|
||||||
content: ["./src/**/*.{html,js,svelte,ts}"],
|
content: ["./src/**/*.{html,js,svelte,ts}"],
|
||||||
safelist: ["dark"],
|
safelist: ["dark"],
|
||||||
|
plugins: [
|
||||||
|
addVariablesForColors
|
||||||
|
],
|
||||||
theme: {
|
theme: {
|
||||||
container: {
|
container: {
|
||||||
center: true,
|
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;
|
export default config;
|
||||||
|
Loading…
Reference in New Issue
Block a user