From 4fde0e61486409a882bf0a9ee1c93548eea7588f Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Thu, 29 May 2025 20:19:01 -0700 Subject: [PATCH] New project + Edit project working * Can fill out metadata * Uploads SVGs for logos * Editor works and persists/loads data --- package-lock.json | 284 +++---- package.json | 5 +- .../migration.sql | 2 + prisma/schema.prisma | 1 + src/assets/styles/variables.scss | 2 + src/lib/components/admin/AdminNavBar.svelte | 322 ++++++++ src/lib/components/admin/AdminPage.svelte | 91 +++ src/lib/components/admin/DataTable.svelte | 11 +- .../admin/DeleteConfirmationModal.svelte | 123 +++ src/lib/components/admin/Editor.svelte | 60 +- .../components/admin/EditorWithUpload.svelte | 503 +++++++++++- src/lib/components/admin/PostDropdown.svelte | 153 ++++ .../admin/ProjectBrandingForm.svelte | 186 +++++ src/lib/components/admin/ProjectForm.svelte | 601 ++++++++++++++ .../components/admin/ProjectListItem.svelte | 258 ++++++ .../admin/ProjectMetadataForm.svelte | 100 +++ .../admin/ProjectStylingForm.svelte | 125 +++ src/lib/components/edra/headless/style.css | 5 +- src/lib/schemas/project.ts | 25 + src/lib/types/project.ts | 58 ++ src/routes/admin/+layout.svelte | 14 +- src/routes/admin/+page.svelte | 242 +----- src/routes/admin/media/+page.svelte | 6 +- src/routes/admin/posts/+page.svelte | 9 +- src/routes/admin/posts/[id]/edit/+page.svelte | 717 ++++++++++++----- src/routes/admin/posts/new/+page.svelte | 666 +++++++++++---- src/routes/admin/projects/+page.svelte | 229 ++++-- .../admin/projects/[id]/edit/+page.svelte | 758 +----------------- src/routes/admin/projects/new/+page.svelte | 5 + src/routes/admin/test-upload/+page.svelte | 6 +- src/routes/api/projects/+server.ts | 1 + src/routes/api/projects/[id]/+server.ts | 1 + svelte.config.js | 2 +- vite.config.ts | 8 +- 34 files changed, 3910 insertions(+), 1669 deletions(-) create mode 100644 prisma/migrations/20250530011034_add_project_logo/migration.sql create mode 100644 src/lib/components/admin/AdminNavBar.svelte create mode 100644 src/lib/components/admin/AdminPage.svelte create mode 100644 src/lib/components/admin/DeleteConfirmationModal.svelte create mode 100644 src/lib/components/admin/PostDropdown.svelte create mode 100644 src/lib/components/admin/ProjectBrandingForm.svelte create mode 100644 src/lib/components/admin/ProjectForm.svelte create mode 100644 src/lib/components/admin/ProjectListItem.svelte create mode 100644 src/lib/components/admin/ProjectMetadataForm.svelte create mode 100644 src/lib/components/admin/ProjectStylingForm.svelte create mode 100644 src/lib/schemas/project.ts create mode 100644 src/lib/types/project.ts create mode 100644 src/routes/admin/projects/new/+page.svelte diff --git a/package-lock.json b/package-lock.json index 87864ba..653864c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -69,7 +69,7 @@ "@poppanator/sveltekit-svg": "^5.0.0-svelte5.4", "@sveltejs/adapter-auto": "^3.0.0", "@sveltejs/kit": "^2.0.0", - "@sveltejs/vite-plugin-svelte": "^3.1.2", + "@sveltejs/vite-plugin-svelte": "^4.0.0-next.6", "@types/eslint": "^8.56.7", "@types/node": "^22.0.2", "autoprefixer": "^10.4.19", @@ -108,7 +108,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" @@ -134,7 +133,6 @@ "cpu": [ "ppc64" ], - "dev": true, "optional": true, "os": [ "aix" @@ -150,7 +148,6 @@ "cpu": [ "arm" ], - "dev": true, "optional": true, "os": [ "android" @@ -166,7 +163,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "android" @@ -182,7 +178,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "android" @@ -198,7 +193,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "darwin" @@ -214,7 +208,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "darwin" @@ -230,7 +223,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "freebsd" @@ -246,7 +238,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "freebsd" @@ -262,7 +253,6 @@ "cpu": [ "arm" ], - "dev": true, "optional": true, "os": [ "linux" @@ -278,7 +268,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -294,7 +283,6 @@ "cpu": [ "ia32" ], - "dev": true, "optional": true, "os": [ "linux" @@ -310,7 +298,6 @@ "cpu": [ "loong64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -326,7 +313,6 @@ "cpu": [ "mips64el" ], - "dev": true, "optional": true, "os": [ "linux" @@ -342,7 +328,6 @@ "cpu": [ "ppc64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -358,7 +343,6 @@ "cpu": [ "riscv64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -374,7 +358,6 @@ "cpu": [ "s390x" ], - "dev": true, "optional": true, "os": [ "linux" @@ -390,7 +373,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -423,7 +405,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "netbsd" @@ -456,7 +437,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "openbsd" @@ -472,7 +452,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "sunos" @@ -488,7 +467,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "win32" @@ -504,7 +482,6 @@ "cpu": [ "ia32" ], - "dev": true, "optional": true, "os": [ "win32" @@ -520,7 +497,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "win32" @@ -1105,7 +1081,6 @@ "version": "0.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", - "dev": true, "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -1119,7 +1094,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, "engines": { "node": ">=6.0.0" } @@ -1128,7 +1102,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "dev": true, "engines": { "node": ">=6.0.0" } @@ -1143,7 +1116,6 @@ "version": "0.3.25", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "dev": true, "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -1205,8 +1177,7 @@ "node_modules/@polka/url": { "version": "1.0.0-next.25", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.25.tgz", - "integrity": "sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ==", - "dev": true + "integrity": "sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ==" }, "node_modules/@poppanator/sveltekit-svg": { "version": "5.0.0-svelte5.4", @@ -1702,6 +1673,15 @@ "win32" ] }, + "node_modules/@sveltejs/acorn-typescript": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.5.tgz", + "integrity": "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==", + "license": "MIT", + "peerDependencies": { + "acorn": "^8.9.0" + } + }, "node_modules/@sveltejs/adapter-auto": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-3.2.2.tgz", @@ -1732,7 +1712,6 @@ "version": "2.5.18", "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.18.tgz", "integrity": "sha512-+g06hvpVAnH7b4CDjhnTDgFWBKBiQJpuSmQeGYOuzbO3SC3tdYjRNlDCrafvDtKbGiT2uxY5Dn9qdEUGVZdWOQ==", - "dev": true, "hasInstallScript": true, "dependencies": { "@types/cookie": "^0.6.0", @@ -1761,59 +1740,43 @@ } }, "node_modules/@sveltejs/vite-plugin-svelte": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-3.1.2.tgz", - "integrity": "sha512-Txsm1tJvtiYeLUVRNqxZGKR/mI+CzuIQuc2gn+YCs9rMTowpNZ2Nqt53JdL8KF9bLhAf2ruR/dr9eZCwdTriRA==", - "dev": true, + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-4.0.4.tgz", + "integrity": "sha512-0ba1RQ/PHen5FGpdSrW7Y3fAMQjrXantECALeOiOdBdzR5+5vPP6HVZRLmZaQL+W8m++o+haIAKq5qT+MiZ7VA==", "license": "MIT", "dependencies": { - "@sveltejs/vite-plugin-svelte-inspector": "^2.1.0", - "debug": "^4.3.4", + "@sveltejs/vite-plugin-svelte-inspector": "^3.0.0-next.0||^3.0.0", + "debug": "^4.3.7", "deepmerge": "^4.3.1", "kleur": "^4.1.5", - "magic-string": "^0.30.10", - "svelte-hmr": "^0.16.0", - "vitefu": "^0.2.5" + "magic-string": "^0.30.12", + "vitefu": "^1.0.3" }, "engines": { - "node": "^18.0.0 || >=20" + "node": "^18.0.0 || ^20.0.0 || >=22" }, "peerDependencies": { - "svelte": "^4.0.0 || ^5.0.0-next.0", + "svelte": "^5.0.0-next.96 || ^5.0.0", "vite": "^5.0.0" } }, "node_modules/@sveltejs/vite-plugin-svelte-inspector": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-2.1.0.tgz", - "integrity": "sha512-9QX28IymvBlSCqsCll5t0kQVxipsfhFFL+L2t3nTWfXnddYwxBuAEtTtlaVQpRz9c37BhJjltSeY4AJSC03SSg==", - "dev": true, + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-3.0.1.tgz", + "integrity": "sha512-2CKypmj1sM4GE7HjllT7UKmo4Q6L5xFRd7VMGEWhYnZ+wc6AUVU01IBd7yUi6WnFndEwWoMNOd6e8UjoN0nbvQ==", "license": "MIT", "dependencies": { - "debug": "^4.3.4" + "debug": "^4.3.7" }, "engines": { - "node": "^18.0.0 || >=20" + "node": "^18.0.0 || ^20.0.0 || >=22" }, "peerDependencies": { - "@sveltejs/vite-plugin-svelte": "^3.0.0", - "svelte": "^4.0.0 || ^5.0.0-next.0", + "@sveltejs/vite-plugin-svelte": "^4.0.0-next.0||^4.0.0", + "svelte": "^5.0.0-next.96 || ^5.0.0", "vite": "^5.0.0" } }, - "node_modules/@sveltejs/vite-plugin-svelte/node_modules/svelte-hmr": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.16.0.tgz", - "integrity": "sha512-Gyc7cOS3VJzLlfj7wKS0ZnzDVdv3Pn2IuVeJPk9m2skfhcu5bq3wtIZyQGggr7/Iim5rH5cncyQft/kRLupcnA==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^12.20 || ^14.13.1 || >= 16" - }, - "peerDependencies": { - "svelte": "^3.19.0 || ^4.0.0" - } - }, "node_modules/@tiptap/core": { "version": "2.12.0", "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.12.0.tgz", @@ -2455,8 +2418,7 @@ "node_modules/@types/cookie": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", - "dev": true + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==" }, "node_modules/@types/eslint": { "version": "8.56.10", @@ -2847,10 +2809,10 @@ } }, "node_modules/acorn": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.0.tgz", - "integrity": "sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==", - "dev": true, + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -2867,15 +2829,6 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/acorn-typescript": { - "version": "1.4.13", - "resolved": "https://registry.npmjs.org/acorn-typescript/-/acorn-typescript-1.4.13.tgz", - "integrity": "sha512-xsc9Xv0xlVfwp2o7sQ+GCQ1PgbkdcpWdTzrwXxO3xDMTAywVS3oXVOcOHuRjAPkS4P9b+yc/qNF15460v+jp4Q==", - "dev": true, - "peerDependencies": { - "acorn": ">=8.9.0" - } - }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -2917,7 +2870,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, + "devOptional": true, "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -2938,12 +2891,12 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "node_modules/aria-query": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", - "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", - "dev": true, - "dependencies": { - "dequal": "^2.0.3" + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" } }, "node_modules/array-union": { @@ -3032,12 +2985,12 @@ "license": "MIT" }, "node_modules/axobject-query": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.0.0.tgz", - "integrity": "sha512-+60uv1hiVFhHZeO+Lz0RYzsVHy5Wr1ayX0mwda9KPDVLNJgZ1T9Ny7VmFbLDzxsH0D87I86vgj3gFrjTJUYznw==", - "dev": true, - "dependencies": { - "dequal": "^2.0.3" + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" } }, "node_modules/balanced-match": { @@ -3058,7 +3011,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, + "devOptional": true, "engines": { "node": ">=8" }, @@ -3086,7 +3039,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, + "devOptional": true, "dependencies": { "fill-range": "^7.1.1" }, @@ -3218,7 +3171,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, + "devOptional": true, "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -3242,7 +3195,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, + "devOptional": true, "dependencies": { "is-glob": "^4.0.1" }, @@ -3263,6 +3216,15 @@ "node": ">=9" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/cluster-key-slot": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", @@ -3362,7 +3324,6 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", - "dev": true, "engines": { "node": ">= 0.6" } @@ -3576,8 +3537,7 @@ "node_modules/devalue": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.0.0.tgz", - "integrity": "sha512-gO+/OMXF7488D+u3ue+G7Y4AA3ZmUnB3eHJXmBTgNHvr4ZNzl36A0ZtG+XCRNYCkYx/bFmw4qtkoFLa+wSrwAA==", - "dev": true + "integrity": "sha512-gO+/OMXF7488D+u3ue+G7Y4AA3ZmUnB3eHJXmBTgNHvr4ZNzl36A0ZtG+XCRNYCkYx/bFmw4qtkoFLa+wSrwAA==" }, "node_modules/devlop": { "version": "1.1.0", @@ -3718,7 +3678,6 @@ "version": "0.21.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", - "dev": true, "hasInstallScript": true, "bin": { "esbuild": "bin/esbuild" @@ -3913,10 +3872,10 @@ } }, "node_modules/esm-env": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.0.0.tgz", - "integrity": "sha512-Cf6VksWPsTuW01vU9Mk/3vRue91Zevka5SjyNf3nEpokFRuqt/KjUQoGAwq9qMmhpLTHmXzSIrFRw8zxWzmFBA==", - "dev": true + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", + "license": "MIT" }, "node_modules/espree": { "version": "10.1.0", @@ -3961,13 +3920,12 @@ } }, "node_modules/esrap": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/esrap/-/esrap-1.2.2.tgz", - "integrity": "sha512-F2pSJklxx1BlQIQgooczXCPHmcWpn6EsP5oo73LQfonG9fIlIENQ8vMmfGXeojP9MrkzUNAfyU5vdFlR9shHAw==", - "dev": true, + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-1.4.6.tgz", + "integrity": "sha512-F/D2mADJ9SHY3IwksD4DAXjTt7qt7GWUf3/8RhCNWmC/67tyb55dpimHmy7EplakFaflV0R/PC+fdSPqrRHAQw==", + "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15", - "@types/estree": "^1.0.1" + "@jridgewell/sourcemap-codec": "^1.4.15" } }, "node_modules/esrecurse": { @@ -4130,7 +4088,7 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, + "devOptional": true, "dependencies": { "to-regex-range": "^5.0.1" }, @@ -4351,8 +4309,7 @@ "node_modules/globalyzer": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/globalyzer/-/globalyzer-0.1.0.tgz", - "integrity": "sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==", - "dev": true + "integrity": "sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==" }, "node_modules/globby": { "version": "11.1.0", @@ -4377,8 +4334,7 @@ "node_modules/globrex": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", - "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", - "dev": true + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==" }, "node_modules/graceful-fs": { "version": "4.2.11", @@ -4509,7 +4465,7 @@ "version": "4.3.6", "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.6.tgz", "integrity": "sha512-Ju0+lEMyzMVZarkTn/gqRpdqd5dOPaz1mCZ0SH3JV6iFw81PldE/PEB1hWVEA288HPt4WXW8O7AWxB10M+03QQ==", - "dev": true + "devOptional": true }, "node_modules/import-fresh": { "version": "3.3.0", @@ -4531,7 +4487,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz", "integrity": "sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==", - "dev": true, "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -4596,7 +4551,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, + "devOptional": true, "dependencies": { "binary-extensions": "^2.0.0" }, @@ -4645,7 +4600,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, + "devOptional": true, "engines": { "node": ">=0.10.0" } @@ -4662,7 +4617,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, + "devOptional": true, "dependencies": { "is-extglob": "^2.1.1" }, @@ -4679,7 +4634,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, + "devOptional": true, "engines": { "node": ">=0.12.0" } @@ -4694,14 +4649,20 @@ } }, "node_modules/is-reference": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz", - "integrity": "sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==", - "dev": true, + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", + "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", + "license": "MIT", "dependencies": { - "@types/estree": "*" + "@types/estree": "^1.0.6" } }, + "node_modules/is-reference/node_modules/@types/estree": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", + "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "license": "MIT" + }, "node_modules/is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", @@ -4887,7 +4848,6 @@ "version": "4.1.5", "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", - "dev": true, "engines": { "node": ">=6" } @@ -4938,8 +4898,7 @@ "node_modules/locate-character": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", - "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", - "dev": true + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==" }, "node_modules/locate-path": { "version": "6.0.0", @@ -5168,7 +5127,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", - "dev": true, "engines": { "node": ">=4" } @@ -5177,7 +5135,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", - "dev": true, "engines": { "node": ">=10" } @@ -5210,7 +5167,6 @@ "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", - "dev": true, "funding": [ { "type": "github", @@ -5286,7 +5242,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, + "devOptional": true, "engines": { "node": ">=0.10.0" } @@ -5497,7 +5453,6 @@ "version": "8.4.39", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz", "integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==", - "dev": true, "funding": [ { "type": "opencollective", @@ -5967,7 +5922,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, + "devOptional": true, "dependencies": { "picomatch": "^2.2.1" }, @@ -6170,7 +6125,6 @@ "version": "1.8.1", "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", - "dev": true, "dependencies": { "mri": "^1.1.0" }, @@ -6220,7 +6174,7 @@ "version": "1.77.8", "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.8.tgz", "integrity": "sha512-4UHg6prsrycW20fqLGPShtEvo/WyHRVRHwOP4DzkUrObWoWI05QBSfzU71TVB7PFaL104TwNaHpjlWXAZbQiNQ==", - "dev": true, + "devOptional": true, "dependencies": { "chokidar": ">=3.0.0 <4.0.0", "immutable": "^4.0.0", @@ -6261,8 +6215,7 @@ "node_modules/set-cookie-parser": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz", - "integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==", - "dev": true + "integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==" }, "node_modules/sharp": { "version": "0.34.2", @@ -6348,7 +6301,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", - "dev": true, "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", @@ -6609,23 +6561,24 @@ } }, "node_modules/svelte": { - "version": "5.0.0-next.169", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.0.0-next.169.tgz", - "integrity": "sha512-8VD4/adoVW5Oo4Kub+jnWnuexAtskjbLuAhy3IGMkSKcQ6RVKEQPo4j+8Xr58epow6ccA7S5zp+PsiTv4eW5Zg==", - "dev": true, + "version": "5.33.6", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.33.6.tgz", + "integrity": "sha512-bxg2QY03JlrilCZmDlshY95Argj0rnX43UQFWZN4fct8PZTNBBmvfow2A6yOW1+YweDjhC2qdZF66ASI0Y21Tw==", + "license": "MIT", "dependencies": { - "@ampproject/remapping": "^2.2.1", - "@jridgewell/sourcemap-codec": "^1.4.15", + "@ampproject/remapping": "^2.3.0", + "@jridgewell/sourcemap-codec": "^1.5.0", + "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", - "acorn": "^8.11.3", - "acorn-typescript": "^1.4.13", - "aria-query": "^5.3.0", - "axobject-query": "^4.0.0", - "esm-env": "^1.0.0", - "esrap": "^1.2.2", - "is-reference": "^3.0.2", + "acorn": "^8.12.1", + "aria-query": "^5.3.1", + "axobject-query": "^4.1.0", + "clsx": "^2.1.1", + "esm-env": "^1.2.1", + "esrap": "^1.4.6", + "is-reference": "^3.0.3", "locate-character": "^3.0.0", - "magic-string": "^0.30.5", + "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" }, "engines": { @@ -6840,7 +6793,6 @@ "version": "0.2.9", "resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz", "integrity": "sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==", - "dev": true, "dependencies": { "globalyzer": "0.1.0", "globrex": "^0.1.2" @@ -6917,7 +6869,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, + "devOptional": true, "dependencies": { "is-number": "^7.0.0" }, @@ -6929,7 +6881,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", - "dev": true, "engines": { "node": ">=6" } @@ -7477,7 +7428,7 @@ "version": "5.5.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", - "dev": true, + "devOptional": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -7598,7 +7549,6 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.2.tgz", "integrity": "sha512-6lA7OBHBlXUxiJxbO5aAY2fsHHzDr1q7DvXYnyZycRs2Dz+dXBWuhpWHvmljTRTpQC2uvGmUFFkSHF2vGo90MA==", - "dev": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.38", @@ -7650,13 +7600,16 @@ } }, "node_modules/vitefu": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-0.2.5.tgz", - "integrity": "sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q==", - "dev": true, + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.0.6.tgz", + "integrity": "sha512-+Rex1GlappUyNN6UfwbVZne/9cYC4+R2XDk9xkNXBKMw6HQagdX9PgZ8V2v1WUSK1wfBLp7qbI1+XSNIlB1xmA==", "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*" + ], "peerDependencies": { - "vite": "^3.0.0 || ^4.0.0 || ^5.0.0" + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" }, "peerDependenciesMeta": { "vite": { @@ -7850,8 +7803,7 @@ "node_modules/zimmerframe": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.2.tgz", - "integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==", - "dev": true + "integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==" }, "node_modules/zod": { "version": "3.25.30", diff --git a/package.json b/package.json index d94ccb1..0dfa595 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "@poppanator/sveltekit-svg": "^5.0.0-svelte5.4", "@sveltejs/adapter-auto": "^3.0.0", "@sveltejs/kit": "^2.0.0", - "@sveltejs/vite-plugin-svelte": "^3.1.2", + "@sveltejs/vite-plugin-svelte": "^4.0.0-next.6", "@types/eslint": "^8.56.7", "@types/node": "^22.0.2", "autoprefixer": "^10.4.19", @@ -101,5 +101,8 @@ }, "prisma": { "seed": "tsx prisma/seed.ts" + }, + "overrides": { + "@sveltejs/vite-plugin-svelte": "^4.0.0-next.6" } } diff --git a/prisma/migrations/20250530011034_add_project_logo/migration.sql b/prisma/migrations/20250530011034_add_project_logo/migration.sql new file mode 100644 index 0000000..28f49d5 --- /dev/null +++ b/prisma/migrations/20250530011034_add_project_logo/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Project" ADD COLUMN "logoUrl" VARCHAR(500); \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index ba5d2fd..6a38ea8 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -22,6 +22,7 @@ model Project { role String? @db.VarChar(255) technologies Json? // Array of tech stack featuredImage String? @db.VarChar(500) + logoUrl String? @db.VarChar(500) gallery Json? // Array of image URLs externalUrl String? @db.VarChar(500) caseStudyContent Json? // BlockNote JSON format diff --git a/src/assets/styles/variables.scss b/src/assets/styles/variables.scss index c2fedf1..61353cb 100644 --- a/src/assets/styles/variables.scss +++ b/src/assets/styles/variables.scss @@ -82,6 +82,8 @@ $red-60: #e33d3d; $red-40: #d31919; $red-00: #3d0c0c; +$salmon-pink: #ffbdb3; // Desaturated salmon pink for hover states + $bg-color: #e8e8e8; $page-color: #ffffff; $card-color: #f7f7f7; diff --git a/src/lib/components/admin/AdminNavBar.svelte b/src/lib/components/admin/AdminNavBar.svelte new file mode 100644 index 0000000..2f8209f --- /dev/null +++ b/src/lib/components/admin/AdminNavBar.svelte @@ -0,0 +1,322 @@ + + + + + diff --git a/src/lib/components/admin/AdminPage.svelte b/src/lib/components/admin/AdminPage.svelte new file mode 100644 index 0000000..45374bc --- /dev/null +++ b/src/lib/components/admin/AdminPage.svelte @@ -0,0 +1,91 @@ + + +
+ + +
+ +
+ + {#if $$slots.fullwidth} +
+ +
+ {/if} +
+ + diff --git a/src/lib/components/admin/DataTable.svelte b/src/lib/components/admin/DataTable.svelte index 26d38cf..f8de45c 100644 --- a/src/lib/components/admin/DataTable.svelte +++ b/src/lib/components/admin/DataTable.svelte @@ -13,6 +13,7 @@ isLoading?: boolean emptyMessage?: string onRowClick?: (item: T) => void + unstyled?: boolean } let { @@ -20,7 +21,8 @@ columns = [], isLoading = false, emptyMessage = 'No data found', - onRowClick + onRowClick, + unstyled = false }: Props = $props() function getCellValue(item: any, column: Column) { @@ -39,7 +41,7 @@ } -
+
{#if isLoading}
@@ -85,6 +87,11 @@ border-radius: 8px; overflow: hidden; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); + + &.unstyled { + border-radius: 0; + box-shadow: none; + } } .loading { diff --git a/src/lib/components/admin/DeleteConfirmationModal.svelte b/src/lib/components/admin/DeleteConfirmationModal.svelte new file mode 100644 index 0000000..d2b1c3e --- /dev/null +++ b/src/lib/components/admin/DeleteConfirmationModal.svelte @@ -0,0 +1,123 @@ + + + + + \ No newline at end of file diff --git a/src/lib/components/admin/Editor.svelte b/src/lib/components/admin/Editor.svelte index 4824fa6..7bdd9d9 100644 --- a/src/lib/components/admin/Editor.svelte +++ b/src/lib/components/admin/Editor.svelte @@ -38,7 +38,7 @@ initialized = true return } - + const json = props.editor.getJSON() data = json onChange?.(json) @@ -72,9 +72,9 @@
- \ No newline at end of file + diff --git a/src/lib/components/admin/EditorWithUpload.svelte b/src/lib/components/admin/EditorWithUpload.svelte index 54fe40e..ce32827 100644 --- a/src/lib/components/admin/EditorWithUpload.svelte +++ b/src/lib/components/admin/EditorWithUpload.svelte @@ -5,7 +5,9 @@ import { EdraToolbar, EdraBubbleMenu } from '$lib/components/edra/headless/index.js' import LoaderCircle from 'lucide-svelte/icons/loader-circle' import { focusEditor, type EdraProps } from '$lib/components/edra/utils.js' - + import EdraToolBarIcon from '$lib/components/edra/headless/components/EdraToolBarIcon.svelte' + import { commands } from '$lib/components/edra/commands/commands.js' + // Import all the same components as Edra import { CodeBlockLowlight } from '@tiptap/extension-code-block-lowlight' import { all, createLowlight } from 'lowlight' @@ -32,15 +34,15 @@ import { IFramePlaceholder } from '$lib/components/edra/extensions/iframe/IFramePlaceholder.js' import { IFrameExtended } from '$lib/components/edra/extensions/iframe/IFrameExtended.js' import IFrameExtendedComponent from '$lib/components/edra/headless/components/IFrameExtended.svelte' - + // Import Edra styles import '$lib/components/edra/headless/style.css' import 'katex/dist/katex.min.css' import '$lib/components/edra/editor.css' import '$lib/components/edra/onedark.css' - + const lowlight = createLowlight(all) - + let { class: className = '', content = undefined, @@ -54,50 +56,192 @@ showToolbar = true, placeholder = 'Type "/" for commands...' }: EdraProps & { showToolbar?: boolean; placeholder?: string } = $props() - + let element = $state() let isLoading = $state(true) + let showTextStyleDropdown = $state(false) + let showMediaDropdown = $state(false) + let dropdownTriggerRef = $state() + let mediaDropdownTriggerRef = $state() + let dropdownPosition = $state({ top: 0, left: 0 }) + let mediaDropdownPosition = $state({ top: 0, left: 0 }) + + // Filter out unwanted commands + const getFilteredCommands = () => { + const filtered = { ...commands } + + // Remove these groups entirely + delete filtered['undo-redo'] + delete filtered['headings'] // In text style dropdown + delete filtered['lists'] // In text style dropdown + delete filtered['alignment'] // Not needed + delete filtered['table'] // Not needed + delete filtered['media'] // Will be in media dropdown + + // Reorganize text-formatting commands + if (filtered['text-formatting']) { + const allCommands = filtered['text-formatting'].commands + const basicFormatting = [] + const advancedFormatting = [] + + // Group basic formatting first + const basicOrder = ['bold', 'italic', 'underline', 'strike'] + basicOrder.forEach(name => { + const cmd = allCommands.find(c => c.name === name) + if (cmd) basicFormatting.push(cmd) + }) + + // Then link and code + const advancedOrder = ['link', 'code'] + advancedOrder.forEach(name => { + const cmd = allCommands.find(c => c.name === name) + if (cmd) advancedFormatting.push(cmd) + }) + + // Create two groups + filtered['basic-formatting'] = { + name: 'Basic Formatting', + label: 'Basic Formatting', + commands: basicFormatting + } + + filtered['advanced-formatting'] = { + name: 'Advanced Formatting', + label: 'Advanced Formatting', + commands: advancedFormatting + } + + // Remove original text-formatting + delete filtered['text-formatting'] + } + + return filtered + } + // Get media commands, but filter out iframe + const getMediaCommands = () => { + if (commands.media) { + return commands.media.commands.filter(cmd => cmd.name !== 'iframe-placeholder') + } + return [] + } + + const filteredCommands = getFilteredCommands() + const colorCommands = commands.colors.commands + const fontCommands = commands.fonts.commands + const excludedCommands = ['colors', 'fonts'] + + // Get current text style for dropdown + const getCurrentTextStyle = (editor: Editor) => { + if (editor.isActive('heading', { level: 1 })) return 'Heading 1' + if (editor.isActive('heading', { level: 2 })) return 'Heading 2' + if (editor.isActive('heading', { level: 3 })) return 'Heading 3' + if (editor.isActive('bulletList')) return 'Bullet List' + if (editor.isActive('orderedList')) return 'Ordered List' + if (editor.isActive('taskList')) return 'Task List' + if (editor.isActive('codeBlock')) return 'Code Block' + if (editor.isActive('blockquote')) return 'Blockquote' + return 'Paragraph' + } + + // Calculate dropdown position + const updateDropdownPosition = () => { + if (dropdownTriggerRef) { + const rect = dropdownTriggerRef.getBoundingClientRect() + dropdownPosition = { + top: rect.bottom + 4, + left: rect.left + } + } + } + + // Toggle dropdown with position update + const toggleDropdown = () => { + if (!showTextStyleDropdown) { + updateDropdownPosition() + } + showTextStyleDropdown = !showTextStyleDropdown + } + + // Update media dropdown position + const updateMediaDropdownPosition = () => { + if (mediaDropdownTriggerRef) { + const rect = mediaDropdownTriggerRef.getBoundingClientRect() + mediaDropdownPosition = { + top: rect.bottom + 4, + left: rect.left + } + } + } + + // Toggle media dropdown + const toggleMediaDropdown = () => { + if (!showMediaDropdown) { + updateMediaDropdownPosition() + } + showMediaDropdown = !showMediaDropdown + } + + // Close dropdown when clicking outside + const handleClickOutside = (event: MouseEvent) => { + const target = event.target as HTMLElement + if (!dropdownTriggerRef?.contains(target) && !target.closest('.dropdown-menu-portal')) { + showTextStyleDropdown = false + } + if (!mediaDropdownTriggerRef?.contains(target) && !target.closest('.media-dropdown-portal')) { + showMediaDropdown = false + } + } + + $effect(() => { + if (showTextStyleDropdown || showMediaDropdown) { + document.addEventListener('click', handleClickOutside) + return () => { + document.removeEventListener('click', handleClickOutside) + } + } + }) + // Custom image paste handler function handleImagePaste(view: any, event: ClipboardEvent) { const item = event.clipboardData?.items[0] - + if (item?.type.indexOf('image') !== 0) { return false } - + const file = item.getAsFile() if (!file) return false - + // Check file size (2MB max) const filesize = file.size / 1024 / 1024 if (filesize > 2) { alert(`Image too large! File size: ${filesize.toFixed(2)} MB (max 2MB)`) return true } - + // Upload to our media API uploadImage(file) - + return true // Prevent default paste behavior } - + async function uploadImage(file: File) { if (!editor) return - + // Create a placeholder while uploading const placeholderSrc = URL.createObjectURL(file) editor.commands.setImage({ src: placeholderSrc }) - + try { const auth = localStorage.getItem('admin_auth') if (!auth) { throw new Error('Not authenticated') } - + const formData = new FormData() formData.append('file', file) - + const response = await fetch('/api/media/upload', { method: 'POST', headers: { @@ -105,17 +249,17 @@ }, body: formData }) - + if (!response.ok) { throw new Error('Upload failed') } - + const media = await response.json() - + // Replace placeholder with actual URL // Set a reasonable default width (max 600px) const displayWidth = media.width && media.width > 600 ? 600 : media.width - + editor.commands.insertContent({ type: 'image', attrs: { @@ -126,7 +270,7 @@ align: 'center' } }) - + // Clean up the object URL URL.revokeObjectURL(placeholderSrc) } catch (error) { @@ -136,7 +280,7 @@ editor.commands.undo() } } - + onMount(() => { editor = initiateEditor( element, @@ -175,18 +319,18 @@ } } ) - + // Add placeholder if (placeholder && editor) { - editor.extensionManager.extensions.find( - ext => ext.name === 'placeholder' - )?.configure({ - placeholder - }) + editor.extensionManager.extensions + .find((ext) => ext.name === 'placeholder') + ?.configure({ + placeholder + }) } - + isLoading = false - + return () => editor?.destroy() }) @@ -194,7 +338,81 @@
{#if showToolbar && editor && !isLoading}
- +
+ +
+ +
+ + + + {#each Object.keys(filteredCommands).filter((key) => !excludedCommands.includes(key)) as keys} + {@const groups = filteredCommands[keys].commands} + {#each groups as command} + + {/each} + + {/each} + + +
+ +
+ + + + { + const color = editor.getAttributes('textStyle').color; + const hasColor = editor.isActive('textStyle', { color }); + if (hasColor) { + editor.chain().focus().unsetColor().run(); + } else { + const color = prompt('Enter the color of the text:'); + if (color !== null) { + editor.chain().focus().setColor(color).run(); + } + } + }} + /> + { + const hasHightlight = editor.isActive('highlight'); + if (hasHightlight) { + editor.chain().focus().unsetHighlight().run(); + } else { + const color = prompt('Enter the color of the highlight:'); + if (color !== null) { + editor.chain().focus().setHighlight({ color }).run(); + } + } + }} + /> +
{/if} {#if editor} @@ -225,6 +443,115 @@ >
+ +{#if showMediaDropdown} +
+ +
+{/if} + + +{#if showTextStyleDropdown} + +{/if} + \ No newline at end of file + + /* Text Style Dropdown Styles */ + .text-style-dropdown { + position: relative; + display: inline-block; + } + + .dropdown-trigger { + display: flex; + align-items: center; + gap: 4px; + padding: 6px 12px; + background: transparent; + border: 1px solid transparent; + border-radius: 8px; + font-size: 14px; + font-family: inherit; + color: var(--edra-text-color); + cursor: pointer; + transition: all 0.2s ease; + min-width: 120px; + justify-content: space-between; + height: 36px; + } + + .dropdown-trigger:hover { + background: rgba(0, 0, 0, 0.06); + border-color: transparent; + } + + .dropdown-menu { + background: white; + border: 1px solid #e0e0e0; + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); + min-width: 160px; + overflow: hidden; + } + + .dropdown-item { + display: flex; + align-items: center; + gap: 8px; + width: 100%; + padding: 8px 16px; + text-align: left; + background: none; + border: none; + font-size: 14px; + font-family: inherit; + color: var(--edra-text-color); + cursor: pointer; + transition: background-color 0.2s ease; + } + + .dropdown-item:hover { + background-color: #f5f5f5; + } + + .dropdown-icon { + flex-shrink: 0; + width: 20px; + height: 20px; + } + + .dropdown-separator { + height: 1px; + background-color: #e0e0e0; + margin: 4px 0; + } + + /* Separator in toolbar */ + :global(.edra-toolbar .separator) { + display: inline-block; + width: 2px; + height: 24px; + background-color: #e0e0e0; + border-radius: 1px; + margin: 0 4px; + vertical-align: middle; + } + + /* Remove default button backgrounds */ + :global(.edra-toolbar button) { + background: transparent; + border: 1px solid transparent; + border-radius: 6px; + transition: all 0.2s ease; + } + + :global(.edra-toolbar button:hover) { + background: rgba(0, 0, 0, 0.06); + border-color: transparent; + } + + :global(.edra-toolbar button.active), + :global(.edra-toolbar button[data-active="true"]) { + background: rgba(0, 0, 0, 0.1); + border-color: transparent; + } + + /* Thicker strokes for icons */ + :global(.edra-toolbar svg) { + stroke-width: 2; + } + diff --git a/src/lib/components/admin/PostDropdown.svelte b/src/lib/components/admin/PostDropdown.svelte new file mode 100644 index 0000000..27b3283 --- /dev/null +++ b/src/lib/components/admin/PostDropdown.svelte @@ -0,0 +1,153 @@ + + + + + \ No newline at end of file diff --git a/src/lib/components/admin/ProjectBrandingForm.svelte b/src/lib/components/admin/ProjectBrandingForm.svelte new file mode 100644 index 0000000..db38ebe --- /dev/null +++ b/src/lib/components/admin/ProjectBrandingForm.svelte @@ -0,0 +1,186 @@ + + +
+

Branding

+ + +
+ {#if formData.logoUrl} +
+ Project logo + +
+ {:else} + + {/if} +
+
+
+ + \ No newline at end of file diff --git a/src/lib/components/admin/ProjectForm.svelte b/src/lib/components/admin/ProjectForm.svelte new file mode 100644 index 0000000..619ddf2 --- /dev/null +++ b/src/lib/components/admin/ProjectForm.svelte @@ -0,0 +1,601 @@ + + + +
+
+ +
+
+ (activeTab = value)} + /> +
+
+ {#if !isLoading} +
+ + + {#if showPublishMenu} +
+ {#if formData.status === 'published'} + + {:else} + + {/if} +
+ {/if} +
+ {/if} +
+
+ +
+ {#if isLoading} +
Loading project...
+ {:else} + {#if error} +
{error}
+ {/if} + + {#if successMessage} +
{successMessage}
+ {/if} + +
+ +
+
+
{ + e.preventDefault() + handleSave() + }} + > + + + + +
+
+ + +
+
+ +
+
+
+ {/if} +
+
+ + \ No newline at end of file diff --git a/src/lib/components/admin/ProjectListItem.svelte b/src/lib/components/admin/ProjectListItem.svelte new file mode 100644 index 0000000..0f705c8 --- /dev/null +++ b/src/lib/components/admin/ProjectListItem.svelte @@ -0,0 +1,258 @@ + + +
e.key === 'Enter' && handleProjectClick()} +> + + +
+

{project.title}

+ +
+ + +
+ + diff --git a/src/lib/components/admin/ProjectMetadataForm.svelte b/src/lib/components/admin/ProjectMetadataForm.svelte new file mode 100644 index 0000000..2f257b0 --- /dev/null +++ b/src/lib/components/admin/ProjectMetadataForm.svelte @@ -0,0 +1,100 @@ + + +
+ + + + + +