diff --git a/client/package-lock.json b/client/package-lock.json index 6001996..318a397 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -8,11 +8,17 @@ "name": "client", "version": "0.0.0", "dependencies": { + "@hookform/resolvers": "^5.2.2", + "@tiptap/extension-underline": "^3.22.5", + "@tiptap/react": "^3.22.5", + "@tiptap/starter-kit": "^3.22.5", "axios": "^1.15.0", - "lucide-react": "^1.11.0", + "lucide-react": "^1.14.0", "react": "^19.2.0", "react-dom": "^19.2.0", - "react-router-dom": "^7.13.1" + "react-hook-form": "^7.75.0", + "react-router-dom": "^7.13.1", + "zod": "^4.4.3" }, "devDependencies": { "@eslint/js": "^9.39.1", @@ -61,7 +67,6 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -912,6 +917,46 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "license": "MIT", + "optional": true + }, + "node_modules/@hookform/resolvers": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.2.2.tgz", + "integrity": "sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==", + "license": "MIT", + "dependencies": { + "@standard-schema/utils": "^0.3.0" + }, + "peerDependencies": { + "react-hook-form": "^7.55.0" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -1371,6 +1416,440 @@ "win32" ] }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, + "node_modules/@tiptap/core": { + "version": "3.22.5", + "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.22.5.tgz", + "integrity": "sha512-L1lhWz6ujGny8LduTJ7MBWYhzigwOvfUJUrJ7IzOJSuy3+OAzisdGDD1GV7LEO/hU0Hr2Mkm1wajRIHExvS9HQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/pm": "3.22.5" + } + }, + "node_modules/@tiptap/extension-blockquote": { + "version": "3.22.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.22.5.tgz", + "integrity": "sha512-ajyP5W8fG5Hrru47T/eF3xMKOpNvWofgNJqBTeNuGl02sYxsy9a4EunyFxudsaZP9WW3VOD4SaIWr5+MqpbnOQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.22.5" + } + }, + "node_modules/@tiptap/extension-bold": { + "version": "3.22.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.22.5.tgz", + "integrity": "sha512-l/uDtpJISiFFyfctvnODNWBN/XPZI1jVZRacTRDDnSn8+x6KQ7G2qgFYueU7KvVJGDFVT39Iio56mcFRG/Pozg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.22.5" + } + }, + "node_modules/@tiptap/extension-bubble-menu": { + "version": "3.22.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.22.5.tgz", + "integrity": "sha512-yrNlFQQJY5MmhBpmD8tnmaSmyUQrEvgyPKa3bzVeWEhDSG1CW4A0ZSMx3hrA9yFO0HWfw3IJmvSCycEZQBalpQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.22.5", + "@tiptap/pm": "3.22.5" + } + }, + "node_modules/@tiptap/extension-bullet-list": { + "version": "3.22.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.22.5.tgz", + "integrity": "sha512-cf54fG9AybU8NgPMv1TOcoqAkELeRc/VpnSCt/rIJZphWQx9nsFmrtkrlCatrIcCaGtNZYwlHlMnC5LVVMu0uA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "3.22.5" + } + }, + "node_modules/@tiptap/extension-code": { + "version": "3.22.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.22.5.tgz", + "integrity": "sha512-mwDNOJC9rYbDu/JcqrN4dbUQRklJU8Fuk2raxD/IvFw9qUIcPCmxQ2XT9UTKmZz/Ju7Kdy72fss6XpgWv6gLAQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.22.5" + } + }, + "node_modules/@tiptap/extension-code-block": { + "version": "3.22.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.22.5.tgz", + "integrity": "sha512-d123kCfLdJTi4fue1m0+TNFztDkmIRSZGZmGu6H9KqwG5Q7IzjT9o8lzRsz+pXxYqHvqgYmXoEpM6srbzXx/Ag==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.22.5", + "@tiptap/pm": "3.22.5" + } + }, + "node_modules/@tiptap/extension-document": { + "version": "3.22.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.22.5.tgz", + "integrity": "sha512-8NJERd+pCtvSuEP4C4WMGYmRRCV12ePZL7bC+QUdFlbdXg+kNZS0zZ7hh879tYA0Kidbi8rWWD1Tx+H2ezkmMw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.22.5" + } + }, + "node_modules/@tiptap/extension-dropcursor": { + "version": "3.22.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.22.5.tgz", + "integrity": "sha512-Mp40DaFrY3sEUVtFqmxrR0BmU4G3k8GCYYNGqNa9OqWv7BrcFDC03V2n3okESDKt4MKkzhQQmypq+ouLy8dLfA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extensions": "3.22.5" + } + }, + "node_modules/@tiptap/extension-floating-menu": { + "version": "3.22.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-3.22.5.tgz", + "integrity": "sha512-dhem4sTPhyQgQ+pFp2Oud4k4FSQz9PVMgeQAC9288SmGwxBkJNveDAw6sKTMrumqDvwkJrtslXIupq9TZYQnzg==", + "license": "MIT", + "optional": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@floating-ui/dom": "^1.0.0", + "@tiptap/core": "3.22.5", + "@tiptap/pm": "3.22.5" + } + }, + "node_modules/@tiptap/extension-gapcursor": { + "version": "3.22.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.22.5.tgz", + "integrity": "sha512-4WkMu7qqjbsm8hCQS+8X+la1wjriN0SKoRdvpfKH33qM50MB34tYJuGLAO+y7TTh4MMMco3AZCKPBL5JVMqNIg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extensions": "3.22.5" + } + }, + "node_modules/@tiptap/extension-hard-break": { + "version": "3.22.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.22.5.tgz", + "integrity": "sha512-n0R2mUVYZU2AVbJhg/WcY9+zx690wVwvsItHJf0DrYbf1tCYHx+PRHUt/AoXk6u8BSmnkb8/FDziS8m3mjfpSg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.22.5" + } + }, + "node_modules/@tiptap/extension-heading": { + "version": "3.22.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.22.5.tgz", + "integrity": "sha512-hjyEG4947PAhMBfP1G6B0QAh6+y9mp2C5BQmNjprA05/lQzDAT7KFZzNh8ZVp3ol6aICKq/N1gFOW9Dc/9FUOw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.22.5" + } + }, + "node_modules/@tiptap/extension-horizontal-rule": { + "version": "3.22.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.22.5.tgz", + "integrity": "sha512-vUV0/ugIbXOc8SJib0h8UMhgcqZXWu/dkEhlswZN4VVven1o5enkfxEiDw+OyIJHi5rUkrdhsQ/KTxG/Xb7X8A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.22.5", + "@tiptap/pm": "3.22.5" + } + }, + "node_modules/@tiptap/extension-italic": { + "version": "3.22.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.22.5.tgz", + "integrity": "sha512-4T8baSiLkeIymTgEwirxDFt5YgYofkP3m1+MGYdGy2HKcOK+1vpvlPhEO1X5qtZngtJW5S4+njKjinRg52A4PA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.22.5" + } + }, + "node_modules/@tiptap/extension-link": { + "version": "3.22.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.22.5.tgz", + "integrity": "sha512-d671MvF3GPKoS2OVxjIlQ7hIE7MS3hREdR+d4cvnnoiLLD+ZJ6KgDnxmWqF0a1s4qxLWK2KxKRSOIfYGE31QWQ==", + "license": "MIT", + "dependencies": { + "linkifyjs": "^4.3.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.22.5", + "@tiptap/pm": "3.22.5" + } + }, + "node_modules/@tiptap/extension-list": { + "version": "3.22.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.22.5.tgz", + "integrity": "sha512-cVO3ZHCgxAWZ4zrFSs81FO2nyCk1wb2EHkpLpW98FzbJLkN9rDkazhW99P3HRWy/CvUldOT+8ecI1YrQtBojMg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.22.5", + "@tiptap/pm": "3.22.5" + } + }, + "node_modules/@tiptap/extension-list-item": { + "version": "3.22.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.22.5.tgz", + "integrity": "sha512-W7uTmyKLhlsvuTPLv+8WwnsY+mlikBFIoLSvVcBaFt4MwpsZ+DeB6KQg02Y7tbtaAnG7rXu9Fvw2QORh2P728A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "3.22.5" + } + }, + "node_modules/@tiptap/extension-list-keymap": { + "version": "3.22.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.22.5.tgz", + "integrity": "sha512-cGUnxJ0y515e1bVHNjUmbx7oWHoEon59w6BA5N2KwV9iW2mZZchlTX4yxJSOX+ixeVRChsa7YwC3Z1jUZ6AMEg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "3.22.5" + } + }, + "node_modules/@tiptap/extension-ordered-list": { + "version": "3.22.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.22.5.tgz", + "integrity": "sha512-OXdh4k4CNrukwiSdWdEQ49uvgnqvR0Z9aNSP4HI5/kZQ/Te1NtRtYCpUrzWyO/7CtjcCisXHti0o9C/TV8YMbQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "3.22.5" + } + }, + "node_modules/@tiptap/extension-paragraph": { + "version": "3.22.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.22.5.tgz", + "integrity": "sha512-52KCto4+XKpnBWpIufspWLyq4UWxAWC72ANPdGuIhbi72NRTabiTbTVN40uwGSPkyakeESG0/vKdWJCVvB4f0g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.22.5" + } + }, + "node_modules/@tiptap/extension-strike": { + "version": "3.22.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.22.5.tgz", + "integrity": "sha512-42WrrFK5gOom/0znH85x12Mw5IQ/6O6DWdyUWoRIrNA/qJpuHtU8oVU+bIgU2tuomMGHruRjIzgBQv5sBjEtww==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.22.5" + } + }, + "node_modules/@tiptap/extension-text": { + "version": "3.22.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.22.5.tgz", + "integrity": "sha512-bzpDOdAEo1JeoVZDIyV0oY0jGXkEG+AzF70SzHoRSjOvFDtKWunyXf9eO1OnOr2/fmMcckT2qwUBNBMQplWBzw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.22.5" + } + }, + "node_modules/@tiptap/extension-underline": { + "version": "3.22.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.22.5.tgz", + "integrity": "sha512-9ut09rJD0iEbS6sk7yd2j6IwuFDLTNmDEGTDLodvqAfi+bq7ddsTDv0YviXoZaA9sdHAdTEVr2ITy2m6WK5jpA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.22.5" + } + }, + "node_modules/@tiptap/extensions": { + "version": "3.22.5", + "resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.22.5.tgz", + "integrity": "sha512-Ifg4MzKCj3uRqe3ieTwYnomu2y4p7EXr2avVSKZYfh12i2dyWe2Gkn1KuZDREANVE+gHqFlQjJRYzhJFwzSCrg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.22.5", + "@tiptap/pm": "3.22.5" + } + }, + "node_modules/@tiptap/pm": { + "version": "3.22.5", + "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.22.5.tgz", + "integrity": "sha512-Cr9Mv4igxvI2tKMiahw48sZxva3PfDzypErH8IB82N+9qa9n9ygVMt0BOaDg53hLKxEEVeYr2S/wCcJIVFgBTw==", + "license": "MIT", + "dependencies": { + "prosemirror-changeset": "^2.3.0", + "prosemirror-commands": "^1.6.2", + "prosemirror-dropcursor": "^1.8.1", + "prosemirror-gapcursor": "^1.3.2", + "prosemirror-history": "^1.4.1", + "prosemirror-keymap": "^1.2.2", + "prosemirror-model": "^1.24.1", + "prosemirror-schema-list": "^1.5.0", + "prosemirror-state": "^1.4.3", + "prosemirror-tables": "^1.6.4", + "prosemirror-transform": "^1.10.2", + "prosemirror-view": "^1.38.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + } + }, + "node_modules/@tiptap/react": { + "version": "3.22.5", + "resolved": "https://registry.npmjs.org/@tiptap/react/-/react-3.22.5.tgz", + "integrity": "sha512-36WHEs+vPmB//V1ff7Ujcnpz7Ey5g8lhpI/0+hoanSbdiPMTQ7qZVWwMovIkMKDlqWVp2fxBgeYM1861jyFzTw==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "fast-equals": "^5.3.3", + "use-sync-external-store": "^1.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "optionalDependencies": { + "@tiptap/extension-bubble-menu": "^3.22.5", + "@tiptap/extension-floating-menu": "^3.22.5" + }, + "peerDependencies": { + "@tiptap/core": "3.22.5", + "@tiptap/pm": "3.22.5", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "@types/react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tiptap/starter-kit": { + "version": "3.22.5", + "resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-3.22.5.tgz", + "integrity": "sha512-LZ/LYbwH6rnDi5DnRyagkuNsYAVyhM+yJvvz+ZuYA0JkPiTXJV86J5PWSKew8M0gVfMHcNVtKjfQCvViFCeIgw==", + "license": "MIT", + "dependencies": { + "@tiptap/core": "^3.22.5", + "@tiptap/extension-blockquote": "^3.22.5", + "@tiptap/extension-bold": "^3.22.5", + "@tiptap/extension-bullet-list": "^3.22.5", + "@tiptap/extension-code": "^3.22.5", + "@tiptap/extension-code-block": "^3.22.5", + "@tiptap/extension-document": "^3.22.5", + "@tiptap/extension-dropcursor": "^3.22.5", + "@tiptap/extension-gapcursor": "^3.22.5", + "@tiptap/extension-hard-break": "^3.22.5", + "@tiptap/extension-heading": "^3.22.5", + "@tiptap/extension-horizontal-rule": "^3.22.5", + "@tiptap/extension-italic": "^3.22.5", + "@tiptap/extension-link": "^3.22.5", + "@tiptap/extension-list": "^3.22.5", + "@tiptap/extension-list-item": "^3.22.5", + "@tiptap/extension-list-keymap": "^3.22.5", + "@tiptap/extension-ordered-list": "^3.22.5", + "@tiptap/extension-paragraph": "^3.22.5", + "@tiptap/extension-strike": "^3.22.5", + "@tiptap/extension-text": "^3.22.5", + "@tiptap/extension-underline": "^3.22.5", + "@tiptap/extensions": "^3.22.5", + "@tiptap/pm": "^3.22.5" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1436,7 +1915,6 @@ "integrity": "sha512-fPxQqz4VTgPI/IQ+lj9r0h+fDR66bzoeMGHp8ASee+32OSGIkeASsoZuJixsQoVef1QJbeubcPBxKk22QVoWdw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -1445,9 +1923,7 @@ "version": "19.2.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -1456,12 +1932,17 @@ "version": "19.2.3", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "dev": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.2.0" } }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.56.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz", @@ -1507,7 +1988,6 @@ "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", @@ -1785,7 +2265,6 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1921,7 +2400,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -2075,7 +2553,6 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, "license": "MIT" }, "node_modules/debug": { @@ -2249,7 +2726,6 @@ "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -2435,6 +2911,15 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-equals": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz", + "integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -2887,6 +3372,12 @@ "node": ">= 0.8.0" } }, + "node_modules/linkifyjs": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.2.tgz", + "integrity": "sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==", + "license": "MIT" + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -2921,9 +3412,9 @@ } }, "node_modules/lucide-react": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.11.0.tgz", - "integrity": "sha512-UOhjdztXCgdBReRcIhsvz2siIBogfv/lhJEIViCpLt924dO+GDms9T7DNoucI23s6kEPpe988m5N0D2ajnzb2g==", + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.14.0.tgz", + "integrity": "sha512-+1mdWcfSJVUsaTIjN9zoezmUhfXo5l0vP7ekBMPo3jcS/aIkxHnXqAPsByszMZx/Y8oQBRJxJx5xg+RH3urzxA==", "license": "ISC", "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -3030,6 +3521,12 @@ "node": ">= 0.8.0" } }, + "node_modules/orderedmap": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz", + "integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==", + "license": "MIT" + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -3108,7 +3605,6 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -3155,6 +3651,135 @@ "node": ">= 0.8.0" } }, + "node_modules/prosemirror-changeset": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.4.1.tgz", + "integrity": "sha512-96WBLhOaYhJ+kPhLg3uW359Tz6I/MfcrQfL4EGv4SrcqKEMC1gmoGrXHecPE8eOwTVCJ4IwgfzM8fFad25wNfw==", + "license": "MIT", + "dependencies": { + "prosemirror-transform": "^1.0.0" + } + }, + "node_modules/prosemirror-commands": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.1.tgz", + "integrity": "sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.10.2" + } + }, + "node_modules/prosemirror-dropcursor": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.2.tgz", + "integrity": "sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.1.0", + "prosemirror-view": "^1.1.0" + } + }, + "node_modules/prosemirror-gapcursor": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.4.1.tgz", + "integrity": "sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==", + "license": "MIT", + "dependencies": { + "prosemirror-keymap": "^1.0.0", + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-view": "^1.0.0" + } + }, + "node_modules/prosemirror-history": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.5.0.tgz", + "integrity": "sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.2.2", + "prosemirror-transform": "^1.0.0", + "prosemirror-view": "^1.31.0", + "rope-sequence": "^1.3.0" + } + }, + "node_modules/prosemirror-keymap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.3.tgz", + "integrity": "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0", + "w3c-keyname": "^2.2.0" + } + }, + "node_modules/prosemirror-model": { + "version": "1.25.4", + "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.4.tgz", + "integrity": "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==", + "license": "MIT", + "dependencies": { + "orderedmap": "^2.0.0" + } + }, + "node_modules/prosemirror-schema-list": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz", + "integrity": "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.7.3" + } + }, + "node_modules/prosemirror-state": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz", + "integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-transform": "^1.0.0", + "prosemirror-view": "^1.27.0" + } + }, + "node_modules/prosemirror-tables": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.8.5.tgz", + "integrity": "sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==", + "license": "MIT", + "dependencies": { + "prosemirror-keymap": "^1.2.3", + "prosemirror-model": "^1.25.4", + "prosemirror-state": "^1.4.4", + "prosemirror-transform": "^1.10.5", + "prosemirror-view": "^1.41.4" + } + }, + "node_modules/prosemirror-transform": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.12.0.tgz", + "integrity": "sha512-GxboyN4AMIsoHNtz5uf2r2Ru551i5hWeCMD6E2Ib4Eogqoub0NflniaBPVQ4MrGE5yZ8JV9tUHg9qcZTTrcN4w==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.21.0" + } + }, + "node_modules/prosemirror-view": { + "version": "1.41.8", + "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.8.tgz", + "integrity": "sha512-TnKDdohEatgyZNGCDWIdccOHXhYloJwbwU+phw/a23KBvJIR9lWQWW7WHHK3vBdOLDNuF7TaX98GObUZOWkOnA==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.20.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.1.0" + } + }, "node_modules/proxy-from-env": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", @@ -3179,7 +3804,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -3189,7 +3813,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -3197,6 +3820,22 @@ "react": "^19.2.4" } }, + "node_modules/react-hook-form": { + "version": "7.75.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.75.0.tgz", + "integrity": "sha512-Ovv94H+0p3sJ7B9B5QxPuCP1u8V/cHuVGyH55cSwodYDtoJwK+fqk3vjfIgSX59I2U/bU4z0nRJ9HMLpNiWEmw==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, "node_modules/react-refresh": { "version": "0.18.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", @@ -3300,6 +3939,12 @@ "fsevents": "~2.3.2" } }, + "node_modules/rope-sequence": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz", + "integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==", + "license": "MIT" + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -3430,7 +4075,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -3511,13 +4155,21 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/vite": { "version": "7.3.2", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -3587,6 +4239,12 @@ } } }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "license": "MIT" + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -3634,12 +4292,10 @@ } }, "node_modules/zod": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", - "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", - "dev": true, + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/client/package.json b/client/package.json index 6016c5f..d4b164b 100644 --- a/client/package.json +++ b/client/package.json @@ -10,11 +10,17 @@ "preview": "vite preview" }, "dependencies": { + "@hookform/resolvers": "^5.2.2", + "@tiptap/extension-underline": "^3.22.5", + "@tiptap/react": "^3.22.5", + "@tiptap/starter-kit": "^3.22.5", "axios": "^1.15.0", - "lucide-react": "^1.11.0", + "lucide-react": "^1.14.0", "react": "^19.2.0", "react-dom": "^19.2.0", - "react-router-dom": "^7.13.1" + "react-hook-form": "^7.75.0", + "react-router-dom": "^7.13.1", + "zod": "^4.4.3" }, "devDependencies": { "@eslint/js": "^9.39.1", diff --git a/client/src/components/EventCard.tsx b/client/src/components/EventCard.tsx new file mode 100644 index 0000000..71eb8f3 --- /dev/null +++ b/client/src/components/EventCard.tsx @@ -0,0 +1,132 @@ +import "../style/common.css"; +import "../style/event.css"; +import { ImageBlock } from "./ImageBlock/ImageBlock"; +import { Clock, MapPin, ArrowRight } from "lucide-react"; +import { useNavigate } from "react-router-dom"; + +// Placeholder Constants +export const DEFAULT_EVENT_IMAGE = "src/images/event-image.png"; +export const DEFAULT_EVENT_LABEL = "UPCOMING EVENT"; +export const DEFAULT_USER_ACTION = "SIGN UP"; +export const DEFAULT_ADMIN_ACTION = "EDIT EVENT"; + +interface EventProps { + id: string; // Added id for routing to extended page + imageUrl: string; + title: string; + time: Date; + location: string; + description: string; + memberPrice?: string; + nonMemberPrice?: string; + role?: "admin" | "user"; + status: "open" | "waitlist" | "ended"; +} + +const EventCard: React.FC = ({ + id, + imageUrl, + title, + time, + location, + description, + memberPrice, + nonMemberPrice, + role = "user", + status, +}) => { + const navigate = useNavigate(); + + // Format date: e.g. "2nd April - 6PM" + const formatDate = (date: Date) => { + const day = date.getDate(); + const month = date.toLocaleString("default", { month: "long" }); + const hours = date.getHours(); + const ampm = hours >= 12 ? "PM" : "AM"; + const hour12 = hours % 12 || 12; + + const getOrdinal = (n: number) => { + const s = ["th", "st", "nd", "rd"]; + const v = n % 100; + return n + (s[(v - 20) % 10] || s[v] || s[0]); + }; + + return `${getOrdinal(day)} ${month} - ${hour12}${ampm}`; + }; + + const handleActionClick = () => { + // Eventually navigate to the extended page + // For now, we'll just log the intent + console.log(`Navigating to extended page for event: ${id} as ${role}`); + navigate(`/Events/${id}`); + }; + + return ( +
+
+
+
+ {DEFAULT_EVENT_LABEL} + + {status.toUpperCase()} + +
+

{title}

+ +
+
+ + {formatDate(time)} +
+
+ + {location} +
+
+ +
+ {memberPrice && ( + {memberPrice} Members + )} + {nonMemberPrice && ( + {nonMemberPrice} Non-Members + )} +
+ +
+ +
+ + +
+
+
+ +
+
+ ); +}; + +export default EventCard; diff --git a/client/src/components/Form/CurrencyInput.tsx b/client/src/components/Form/CurrencyInput.tsx new file mode 100644 index 0000000..b4574c5 --- /dev/null +++ b/client/src/components/Form/CurrencyInput.tsx @@ -0,0 +1,62 @@ +import React from "react"; +import type { UseFormRegisterReturn } from "react-hook-form"; + +interface CurrencyInputProps { + register: UseFormRegisterReturn; + placeholder?: string; +} + +const CurrencyInput: React.FC = ({ + register, + placeholder, +}) => { + const handlePriceKeyDown = (e: React.KeyboardEvent) => { + const isNumber = /\d/.test(e.key); + const isDot = e.key === "."; + const isNavigation = [ + "Backspace", + "Delete", + "ArrowLeft", + "ArrowRight", + "Tab", + "Enter", + ].includes(e.key); + + if (isNavigation || e.ctrlKey || e.metaKey) return; + + const currentVal = e.currentTarget.value; + const selectionStart = e.currentTarget.selectionStart ?? 0; + + if (isDot && currentVal.includes(".")) { + e.preventDefault(); + return; + } + + if (currentVal.includes(".") && isNumber) { + const dotIndex = currentVal.indexOf("."); + const decimals = currentVal.split(".")[1]; + if (decimals && decimals.length >= 2 && selectionStart > dotIndex) { + e.preventDefault(); + return; + } + } + + if (!isNumber && !isDot) { + e.preventDefault(); + } + }; + + return ( +
+ $ + +
+ ); +}; + +export default CurrencyInput; diff --git a/client/src/components/Form/FormField.tsx b/client/src/components/Form/FormField.tsx new file mode 100644 index 0000000..f8b7128 --- /dev/null +++ b/client/src/components/Form/FormField.tsx @@ -0,0 +1,25 @@ +import React from "react"; + +interface FormFieldProps { + label: string; + error?: string; + children: React.ReactNode; + className?: string; +} + +const FormField: React.FC = ({ + label, + error, + children, + className = "", +}) => { + return ( +
+ + {children} + {error && {error}} +
+ ); +}; + +export default FormField; diff --git a/client/src/components/RichTextEditor.tsx b/client/src/components/RichTextEditor.tsx new file mode 100644 index 0000000..d9461cb --- /dev/null +++ b/client/src/components/RichTextEditor.tsx @@ -0,0 +1,115 @@ +import { useEditor, EditorContent } from "@tiptap/react"; +import StarterKit from "@tiptap/starter-kit"; +import Underline from "@tiptap/extension-underline"; +import { useEffect, useState } from "react"; +import { + Bold, + Italic, + Underline as UnderlineIcon, + List, + ListOrdered, + Undo, + Redo, +} from "lucide-react"; + +interface RichTextEditorProps { + value: string; + onChange: (value: string) => void; +} + +const RichTextEditor = ({ value, onChange }: RichTextEditorProps) => { + const [, setUpdateCount] = useState(0); + + const editor = useEditor({ + extensions: [StarterKit, Underline], + content: value, + onUpdate: ({ editor }) => { + onChange(editor.getHTML()); + }, + onTransaction: () => { + setUpdateCount((prev) => prev + 1); + }, + }); + + useEffect(() => { + if (editor && value !== editor.getHTML()) { + editor.commands.setContent(value); + } + }, [value, editor]); + + if (!editor) { + return null; + } + + return ( +
+
+ + + + +
+ + + + +
+ + + +
+ +
+ ); +}; + +export default RichTextEditor; diff --git a/client/src/main/App.tsx b/client/src/main/App.tsx index d20402b..cd1ff8c 100644 --- a/client/src/main/App.tsx +++ b/client/src/main/App.tsx @@ -15,6 +15,7 @@ import Contact from "../pages/Contact.tsx"; import Sponsors from "../pages/Sponsors.tsx"; import Events from "../pages/Events.tsx"; import About from "../pages/About.tsx"; +import AdminEventEditor from "../pages/AdminEventEditor.tsx"; import SignUp from "../pages/Signup.tsx"; const App = () => { @@ -29,6 +30,7 @@ const App = () => { } /> } /> } /> + } /> } /> } /> } /> diff --git a/client/src/pages/AdminEventEditor.tsx b/client/src/pages/AdminEventEditor.tsx new file mode 100644 index 0000000..dc3e1c9 --- /dev/null +++ b/client/src/pages/AdminEventEditor.tsx @@ -0,0 +1,224 @@ +import { useForm, Controller, useWatch } from "react-hook-form"; +import { useState } from "react"; +import { useParams, useNavigate } from "react-router-dom"; +import { zodResolver } from "@hookform/resolvers/zod"; +import * as z from "zod"; + +import EventCard from "../components/EventCard"; +import { ImageBlock } from "../components/ImageBlock/ImageBlock"; +import RichTextEditor from "../components/RichTextEditor"; +import FormField from "../components/Form/FormField"; +import CurrencyInput from "../components/Form/CurrencyInput"; + +import "../style/common.css"; +import "../style/editor.css"; + +const eventSchema = z.object({ + title: z + .string() + .min(5, "Title must be at least 5 characters") + .max(100, "Title is too long"), + time: z.string().min(1, "Date and time are required"), + location: z + .string() + .min(1, "Location is required") + .max(150, "Location is too long"), + description: z.string().min(20, "Description must be at least 20 characters"), + memberPrice: z + .string() + .regex(/^\d*(\.\d{1,2})?$/, "Enter a valid amount") + .optional() + .or(z.literal("")), + nonMemberPrice: z + .string() + .regex(/^\d*(\.\d{1,2})?$/, "Enter a valid amount") + .optional() + .or(z.literal("")), + imageTag: z.string(), + status: z.enum(["open", "waitlist", "ended"]), +}); + +type EventFormData = z.infer; + +const AdminEventEditor = () => { + const { eventId } = useParams(); + const navigate = useNavigate(); + const [isPreview, setIsPreview] = useState(false); + + const isEditMode = eventId && eventId !== "new"; + + const { + register, + handleSubmit, + control, + formState: { errors }, + } = useForm({ + resolver: zodResolver(eventSchema), + mode: "onChange", + defaultValues: { + title: "", + time: "", + location: "", + description: "", + memberPrice: "", + nonMemberPrice: "", + status: "open", + imageTag: isEditMode ? eventId : "new-event", + }, + }); + + const formData = useWatch({ control }); + + const onSubmit = (data: EventFormData) => { + const finalData = { + ...data, + memberPrice: data.memberPrice ? `$${data.memberPrice}` : "", + nonMemberPrice: data.nonMemberPrice ? `$${data.nonMemberPrice}` : "", + }; + console.log(`${isEditMode ? "Updating" : "Creating"} Event:`, { + id: eventId, + ...finalData, + }); + }; + + return ( +
+
+
+

{isEditMode ? "Edit Event" : "Create Event"}

+ +
+ + {isPreview ? ( +
+ +
+ ) : ( +
+ + + + + + + + + + + + + + + + + + + + + + + + + + ( + + )} + /> + + +
+ + +
+ +
+ + +
+
+ )} +
+
+ ); +}; + +export default AdminEventEditor; diff --git a/client/src/pages/Events.tsx b/client/src/pages/Events.tsx index 1da853e..68a6f8b 100644 --- a/client/src/pages/Events.tsx +++ b/client/src/pages/Events.tsx @@ -1,5 +1,56 @@ +import { Link } from "react-router-dom"; +import EventCard from "../components/EventCard.tsx"; +import eventsData from "../placeholders/events.json"; + +const PAGE_TITLE = "Upcoming Events"; + const Events = () => { - return
Events page - not yet implemented
; + const isAdmin = true; // Hardcoded for dev preview + + return ( +
+

+ {PAGE_TITLE} +

+ + {isAdmin && ( + + + Create New Event + + )} + + {eventsData.map((event) => ( + + ))} +
+ ); }; export default Events; diff --git a/client/src/placeholders/events.json b/client/src/placeholders/events.json new file mode 100644 index 0000000..6e01383 --- /dev/null +++ b/client/src/placeholders/events.json @@ -0,0 +1,13 @@ +[ + { + "id": "ice-kachang-2026", + "title": "Ice Kachang", + "time": "2026-04-02T18:00:00", + "location": "401-318 Engineering Atrium (Level 3)", + "description": "Hot, stressed and over Uni already? Say less... we've got the perfect cooldown for you. Come chill with SSA at our Ice Kachang Night. Sweet, icy, colourful... but there's a twist 👀", + "memberPrice": "$5", + "nonMemberPrice": "$11", + "imageUrl": "", + "status": "open" + } +] diff --git a/client/src/style/editor.css b/client/src/style/editor.css new file mode 100644 index 0000000..ad9d90c --- /dev/null +++ b/client/src/style/editor.css @@ -0,0 +1,233 @@ +.editor-container { + max-width: 900px; + margin: 40px auto; + padding: 40px; + background: white; + border-radius: 40px; + border: 1px solid; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05); +} + +.editor-header { + margin-bottom: 30px; + border-bottom: 1px solid; + padding-bottom: 20px; + display: flex; + justify-content: space-between; + align-items: center; +} + +.editor-header h1 { + margin: 0; + font-size: 2.5rem; +} + +.editor-form { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 20px; +} + +.form-group { + display: flex; + flex-direction: column; + gap: 8px; +} + +.form-group.full-width { + grid-column: span 2; +} + +.form-group label { + font-weight: 700; + font-size: 0.85rem; + text-transform: uppercase; +} + +.form-group input, +.form-group textarea, +.form-group select { + padding: 12px; + border-radius: 12px; + border: 1px solid; + font-size: 1rem; + font-family: inherit; +} + +/* Currency Input Prefix Styles */ +.currency-input-wrapper { + display: flex; + align-items: center; + border: 1px solid black; + border-radius: 12px; + background: white; + overflow: hidden; +} + +.currency-prefix { + padding: 0 12px; + background: #f0f0f0; + height: 100%; + display: flex; + align-items: center; + font-weight: 700; + border-right: 1px solid #ddd; + user-select: none; +} + +.currency-input-wrapper input { + border: none !important; + border-radius: 0 !important; + flex: 1; + padding-left: 8px !important; +} + +.form-group textarea { + height: 120px; + resize: vertical; +} + +/* Tiptap Editor Styles */ +.tiptap-editor-container { + border: 1px solid black; + border-radius: 12px; + overflow: hidden; + background: white; + margin-top: 5px; +} + +.editor-toolbar { + display: flex; + flex-direction: row; /* Force horizontal */ + flex-wrap: wrap; + gap: 2px; + padding: 6px; + border-bottom: 1px solid black; + background: #fdfdfd; + align-items: center; +} + +.editor-toolbar button { + background: transparent; + border: none; + padding: 8px; + border-radius: 6px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; + color: #444; + width: auto; /* Override global button width: 100% */ +} + +.editor-toolbar button:hover { + background: #f0f0f0; + color: black; +} + +.editor-toolbar button.is-active { + background: black; + color: white; +} + +.editor-toolbar .divider { + width: 1px; + height: 24px; + background: #eee; + margin: 0 8px; +} + +.tiptap-content { + padding: 16px; + min-height: 200px; + max-height: 400px; + overflow-y: auto; + font-size: 1rem; + line-height: 1.6; +} + +.ProseMirror { + outline: none; +} + +.ProseMirror p.is-editor-empty:first-child::before { + content: attr(data-placeholder); + float: left; + color: #adb5bd; + pointer-events: none; + height: 0; +} + +.error-text { + color: #ff4d4f; + font-size: 0.8rem; + font-weight: 600; + margin-top: -4px; +} + +.preview-section { + grid-column: span 2; + padding: 20px; + background: #f9f9f9; + border-radius: 20px; + border: 1px dashed #ccc; +} + +.editor-actions { + grid-column: span 2; + display: flex; + gap: 15px; + margin-top: 20px; +} + +.btn-save { + background: black; + color: white; + padding: 15px 30px; + border-radius: 30px; + border: none; + font-weight: 700; + cursor: pointer; + text-transform: uppercase; + transition: 0.2s; +} + +.btn-save:hover { + background: #333; + transform: translateY(-2px); +} + +.btn-cancel { + background: transparent; + border: 1px solid; + padding: 15px 30px; + border-radius: 30px; + font-weight: 700; + cursor: pointer; + text-transform: uppercase; + transition: 0.2s; +} + +.btn-cancel:hover { + background: #eee; +} + +@media (max-width: 768px) { + .editor-form { + grid-template-columns: 1fr; + } + .form-group.full-width { + grid-column: span 1; + } + .editor-container { + padding: 20px; + border-radius: 20px; + margin: 20px; + } + .editor-header { + flex-direction: column; + gap: 15px; + align-items: flex-start; + } +} diff --git a/client/src/style/event.css b/client/src/style/event.css new file mode 100644 index 0000000..c8eea4b --- /dev/null +++ b/client/src/style/event.css @@ -0,0 +1,154 @@ +.event-card { + display: flex; + background: white; + border-radius: 40px; + padding: 40px; + max-width: 1000px; + margin: 40px auto; + gap: 40px; + border: 1px solid; +} + +.event-content { + flex: 1; + min-width: 0; +} + +.event-label { + font-size: 0.9rem; + font-weight: 700; +} + +.event-status { + padding: 4px 12px; + border-radius: 12px; + font-size: 0.75rem; + font-weight: 800; + letter-spacing: 0.05em; +} + +.status-open { + background-color: #e6f7ed; + color: #2e7d32; + border: 1px solid #a5d6a7; +} + +.status-waitlist { + background-color: #fff9e6; + color: #f57c00; + border: 1px solid #ffe082; +} + +.status-ended { + background-color: #f5f5f5; + color: #757575; + border: 1px solid #bdbdbd; +} + +.event-title { + font-size: 2.8rem; + margin: 10px 0 20px; +} + +.event-meta, +.meta-item { + display: flex; + flex-direction: column; + gap: 12px; +} + +.meta-item { + flex-direction: row; + align-items: center; + font-size: 1.1rem; + font-weight: 500; +} + +.event-tags { + display: flex; + gap: 12px; + margin: 20px 0 30px; +} + +.event-tag { + background: black; + color: white; + padding: 8px 18px; + border-radius: 20px; + font-size: 0.95rem; + font-weight: 600; +} + +.event-divider { + border: 0; + border-top: 1px solid; + margin-bottom: 25px; +} + +.event-description { + line-height: 1.6; + font-size: 1.15rem; + margin-bottom: 35px; + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; + word-break: break-word; + overflow-wrap: anywhere; +} + +.event-description > * { + display: inline; + margin: 0; +} + +.rsvp-button { + border: 1px solid; + padding: 14px 24px; + border-radius: 35px; + text-decoration: none; + font-weight: 700; + display: flex; + align-items: center; + justify-content: center; + gap: 12px; + max-width: 400px; + transition: 0.2s; + text-transform: uppercase; +} + +.rsvp-button:hover { + background: #faf3d1; + transform: translateY(-2px); +} + +.event-image-container { + flex: 0 0 40%; + min-width: 320px; +} + +.event-image-container img { + width: 100%; + height: 100%; + object-fit: cover; + border-radius: 20px; + display: block; +} + +@media (max-width: 900px) { + .event-card { + flex-direction: column-reverse; + padding: 30px; + border-radius: 30px; + } + .event-image-container { + height: 300px; + } + .event-title { + font-size: 2.2rem; + } + .rsvp-button { + max-width: none; + } +}