diff --git a/package-lock.json b/package-lock.json index a47eedc..24eae4f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,13 +8,11 @@ "name": "aio-shadcn-vite", "version": "0.0.0", "dependencies": { - "@internationalized/date": "^3.12.1", "@tanstack/vue-table": "^8.21.3", "@vee-validate/zod": "^4.15.1", "@vueuse/components": "^12.5.0", "@vueuse/core": "^12.8.2", "@vueuse/integrations": "^13.6.0", - "browser-image-compression": "^2.0.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^4.1.0", @@ -29,7 +27,7 @@ "qr-scanner": "^1.4.2", "qrcode": "^1.5.4", "radix-vue": "^1.9.13", - "reka-ui": "^2.9.7", + "reka-ui": "^2.6.0", "tailwind-merge": "^2.6.0", "tailwindcss-animate": "^1.0.7", "unique-names-generator": "^4.7.1", @@ -147,6 +145,7 @@ "integrity": "sha512-l+lkXCHS6tQEc5oUpK28xBOZ6+HwaH7YwoYQbLFiYb4nS2/l1tKnZEtEWkD0GuiYdvArf9qBS0XlQGXzPMsNqQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.2", @@ -2652,6 +2651,7 @@ "integrity": "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "chalk": "^4.1.1", "fs-extra": "^9.0.1", @@ -3949,9 +3949,9 @@ } }, "node_modules/@internationalized/date": { - "version": "3.12.1", - "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.12.1.tgz", - "integrity": "sha512-6IedsVWXyq4P9Tj+TxuU8WGWM70hYLl12nbYU8jkikVpa6WXapFazPUcHUMDMoWftIDE2ILDkFFte6W2nFCkRQ==", + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.7.0.tgz", + "integrity": "sha512-VJ5WS3fcVx0bejE/YHfbDKR/yawZgKqn/if+oEeLqNwBtPzVB06olkfcnojTmEMX+gTpH+FlQ69SHNitJ8/erQ==", "license": "Apache-2.0", "dependencies": { "@swc/helpers": "^0.5.0" @@ -5718,6 +5718,7 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -6060,15 +6061,6 @@ "node": ">=8" } }, - "node_modules/browser-image-compression": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/browser-image-compression/-/browser-image-compression-2.0.2.tgz", - "integrity": "sha512-pBLlQyUf6yB8SmmngrcOw3EoS4RpQ1BcylI3T9Yqn7+4nrQTXJD4sJDe5ODnJdrvNMaio5OicFo75rDyJD2Ucw==", - "license": "MIT", - "dependencies": { - "uzip": "0.20201231.0" - } - }, "node_modules/browserslist": { "version": "4.24.4", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", @@ -6089,6 +6081,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001688", "electron-to-chromium": "^1.5.73", @@ -7055,9 +7048,9 @@ } }, "node_modules/defu": { - "version": "6.1.7", - "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", - "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", "license": "MIT" }, "node_modules/detect-libc": { @@ -7635,17 +7628,6 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "license": "MIT" }, - "node_modules/encoding": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", - "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "iconv-lite": "^0.6.2" - } - }, "node_modules/end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -8399,6 +8381,7 @@ "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.0.0.tgz", "integrity": "sha512-14F4hBIxqKvD4Zz/XjDc3y94mNZN6pRv3U13Udo0lNLCWRBUsrMv2xwcF/y/Z5sV6+FQW+/ow68cHpm4sunt8Q==", "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=10" } @@ -8935,20 +8918,6 @@ "ms": "^2.0.0" } }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/idb": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", @@ -11764,6 +11733,7 @@ "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", "license": "MIT", + "peer": true, "dependencies": { "dijkstrajs": "^1.0.1", "pngjs": "^5.0.0", @@ -12226,9 +12196,9 @@ } }, "node_modules/reka-ui": { - "version": "2.9.7", - "resolved": "https://registry.npmjs.org/reka-ui/-/reka-ui-2.9.7.tgz", - "integrity": "sha512-aX7foYYR20v4+majO58OJJdBNfLMm0eJb448l9N4JVy8JB7GXOr4H/S4a+J1pkcoxZH8Cb7YHpJ855+miAm7sA==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/reka-ui/-/reka-ui-2.6.0.tgz", + "integrity": "sha512-NrGMKrABD97l890mFS3TNUzB0BLUfbL3hh0NjcJRIUSUljb288bx3Mzo31nOyUcdiiW0HqFGXJwyCBh9cWgb0w==", "license": "MIT", "dependencies": { "@floating-ui/dom": "^1.6.13", @@ -12236,62 +12206,26 @@ "@internationalized/date": "^3.5.0", "@internationalized/number": "^3.5.0", "@tanstack/vue-virtual": "^3.12.0", - "@vueuse/core": "^14.1.0", - "@vueuse/shared": "^14.1.0", + "@vueuse/core": "^12.5.0", + "@vueuse/shared": "^12.5.0", "aria-hidden": "^1.2.4", - "defu": "^6.1.5", + "defu": "^6.1.4", "ohash": "^2.0.11" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/zernonia" - }, "peerDependencies": { - "vue": ">= 3.4.0" - } - }, - "node_modules/reka-ui/node_modules/@types/web-bluetooth": { - "version": "0.0.21", - "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", - "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", - "license": "MIT" - }, - "node_modules/reka-ui/node_modules/@vueuse/core": { - "version": "14.3.0", - "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.3.0.tgz", - "integrity": "sha512-aHfz47g0ZhMtTVHmIzMVpJy8ePhhOy68GY5bv110+5DVtZ+W7BsOx+m61UNQqfrWyPztIHIanWa3E2tib3NFIw==", - "license": "MIT", - "dependencies": { - "@types/web-bluetooth": "^0.0.21", - "@vueuse/metadata": "14.3.0", - "@vueuse/shared": "14.3.0" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - }, - "peerDependencies": { - "vue": "^3.5.0" - } - }, - "node_modules/reka-ui/node_modules/@vueuse/metadata": { - "version": "14.3.0", - "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.3.0.tgz", - "integrity": "sha512-BwxmbAzwAVF50+MW57GXOUEV61nFBGnlBvrTqj49PqWJu3uw7hdu72ztXeZ33RdZtDY6kO+bfCAE1PCn88Tktw==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/antfu" + "vue": ">= 3.2.0" } }, "node_modules/reka-ui/node_modules/@vueuse/shared": { - "version": "14.3.0", - "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.3.0.tgz", - "integrity": "sha512-bZpge9eSXwa4ToSiqJ7j6KRwhAsneMFoSz3LMWKQDkqimm3D/tbFlrklrs/IOqC8tEcYmXQZJ6N0UrjhBirVCg==", + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-12.8.2.tgz", + "integrity": "sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==", "license": "MIT", + "dependencies": { + "vue": "^3.5.13" + }, "funding": { "url": "https://github.com/sponsors/antfu" - }, - "peerDependencies": { - "vue": "^3.5.0" } }, "node_modules/require-directory": { @@ -12443,6 +12377,7 @@ "integrity": "sha512-4iya7Jb76fVpQyLoiVpzUrsjQ12r3dM7fIVz+4NwoYvZOShknRmiv+iu9CClZml5ZLGb0XMcYLutK6w9tgxHDw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -13452,7 +13387,8 @@ "version": "4.0.12", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.12.tgz", "integrity": "sha512-bT0hJo91FtncsAMSsMzUkoo/iEU0Xs5xgFgVC9XmdM9bw5MhZuQFjPNl6wxAE0SiQF/YTZJa+PndGWYSDtuxAg==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/tailwindcss-animate": { "version": "1.0.7", @@ -13587,6 +13523,7 @@ "integrity": "sha512-GWANVlPM/ZfYzuPHjq0nxT+EbOEDDN3Jwhwdg1D8TU8oSkktp8w64Uq4auuGLxFSoNTRDncTq2hQHX1Ld9KHkA==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", @@ -13816,6 +13753,7 @@ "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -14015,12 +13953,6 @@ "dev": true, "license": "MIT" }, - "node_modules/uzip": { - "version": "0.20201231.0", - "resolved": "https://registry.npmjs.org/uzip/-/uzip-0.20201231.0.tgz", - "integrity": "sha512-OZeJfZP+R0z9D6TmBgLq2LHzSSptGMGDGigGiEe0pr8UBe/7fdflgHlHBNDASTXB5jnFuxHpNaJywSg8YFeGng==", - "license": "MIT" - }, "node_modules/validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", @@ -14072,6 +14004,7 @@ "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -14296,6 +14229,7 @@ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz", "integrity": "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==", "license": "MIT", + "peer": true, "dependencies": { "@vue/compiler-dom": "3.5.13", "@vue/compiler-sfc": "3.5.13", @@ -14748,6 +14682,7 @@ "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "rollup": "dist/bin/rollup" }, @@ -15005,6 +14940,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index 36cf2cf..d7db78c 100644 --- a/package.json +++ b/package.json @@ -43,13 +43,11 @@ "make": "electron-forge make" }, "dependencies": { - "@internationalized/date": "^3.12.1", "@tanstack/vue-table": "^8.21.3", "@vee-validate/zod": "^4.15.1", "@vueuse/components": "^12.5.0", "@vueuse/core": "^12.8.2", "@vueuse/integrations": "^13.6.0", - "browser-image-compression": "^2.0.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^4.1.0", @@ -64,7 +62,7 @@ "qr-scanner": "^1.4.2", "qrcode": "^1.5.4", "radix-vue": "^1.9.13", - "reka-ui": "^2.9.7", + "reka-ui": "^2.6.0", "tailwind-merge": "^2.6.0", "tailwindcss-animate": "^1.0.7", "unique-names-generator": "^4.7.1", diff --git a/src/components/ui/calendar/Calendar.vue b/src/components/ui/calendar/Calendar.vue deleted file mode 100644 index d112cf3..0000000 --- a/src/components/ui/calendar/Calendar.vue +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - - - - - - - - - - - {{ day }} - - - - - - - - - - - - - - diff --git a/src/components/ui/calendar/CalendarCell.vue b/src/components/ui/calendar/CalendarCell.vue deleted file mode 100644 index 53ff2d9..0000000 --- a/src/components/ui/calendar/CalendarCell.vue +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - diff --git a/src/components/ui/calendar/CalendarCellTrigger.vue b/src/components/ui/calendar/CalendarCellTrigger.vue deleted file mode 100644 index a4beb75..0000000 --- a/src/components/ui/calendar/CalendarCellTrigger.vue +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - diff --git a/src/components/ui/calendar/CalendarGrid.vue b/src/components/ui/calendar/CalendarGrid.vue deleted file mode 100644 index 5f52519..0000000 --- a/src/components/ui/calendar/CalendarGrid.vue +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - diff --git a/src/components/ui/calendar/CalendarGridBody.vue b/src/components/ui/calendar/CalendarGridBody.vue deleted file mode 100644 index 4fe36d7..0000000 --- a/src/components/ui/calendar/CalendarGridBody.vue +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - diff --git a/src/components/ui/calendar/CalendarGridHead.vue b/src/components/ui/calendar/CalendarGridHead.vue deleted file mode 100644 index 376d70b..0000000 --- a/src/components/ui/calendar/CalendarGridHead.vue +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - diff --git a/src/components/ui/calendar/CalendarGridRow.vue b/src/components/ui/calendar/CalendarGridRow.vue deleted file mode 100644 index ae99082..0000000 --- a/src/components/ui/calendar/CalendarGridRow.vue +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - diff --git a/src/components/ui/calendar/CalendarHeadCell.vue b/src/components/ui/calendar/CalendarHeadCell.vue deleted file mode 100644 index 911f909..0000000 --- a/src/components/ui/calendar/CalendarHeadCell.vue +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - diff --git a/src/components/ui/calendar/CalendarHeader.vue b/src/components/ui/calendar/CalendarHeader.vue deleted file mode 100644 index 706f78b..0000000 --- a/src/components/ui/calendar/CalendarHeader.vue +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - diff --git a/src/components/ui/calendar/CalendarHeading.vue b/src/components/ui/calendar/CalendarHeading.vue deleted file mode 100644 index 3b84ee8..0000000 --- a/src/components/ui/calendar/CalendarHeading.vue +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - {{ headingValue }} - - - diff --git a/src/components/ui/calendar/CalendarNextButton.vue b/src/components/ui/calendar/CalendarNextButton.vue deleted file mode 100644 index ae8861c..0000000 --- a/src/components/ui/calendar/CalendarNextButton.vue +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - diff --git a/src/components/ui/calendar/CalendarPrevButton.vue b/src/components/ui/calendar/CalendarPrevButton.vue deleted file mode 100644 index 43e32a0..0000000 --- a/src/components/ui/calendar/CalendarPrevButton.vue +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - diff --git a/src/components/ui/calendar/index.ts b/src/components/ui/calendar/index.ts deleted file mode 100644 index f222de0..0000000 --- a/src/components/ui/calendar/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -export { default as Calendar } from "./Calendar.vue" -export { default as CalendarCell } from "./CalendarCell.vue" -export { default as CalendarCellTrigger } from "./CalendarCellTrigger.vue" -export { default as CalendarGrid } from "./CalendarGrid.vue" -export { default as CalendarGridBody } from "./CalendarGridBody.vue" -export { default as CalendarGridHead } from "./CalendarGridHead.vue" -export { default as CalendarGridRow } from "./CalendarGridRow.vue" -export { default as CalendarHeadCell } from "./CalendarHeadCell.vue" -export { default as CalendarHeader } from "./CalendarHeader.vue" -export { default as CalendarHeading } from "./CalendarHeading.vue" -export { default as CalendarNextButton } from "./CalendarNextButton.vue" -export { default as CalendarPrevButton } from "./CalendarPrevButton.vue" diff --git a/src/modules/activities/components/CreateEventDialog.vue b/src/modules/activities/components/CreateEventDialog.vue index 1855049..3eb5b2a 100644 --- a/src/modules/activities/components/CreateEventDialog.vue +++ b/src/modules/activities/components/CreateEventDialog.vue @@ -24,6 +24,7 @@ import { Input } from '@/components/ui/input' import { Textarea } from '@/components/ui/textarea' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' +import { Separator } from '@/components/ui/separator' import { ScrollArea } from '@/components/ui/scroll-area' import { Select, @@ -32,13 +33,14 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select' -import { Calendar, Loader2, MapPin } from 'lucide-vue-next' +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from '@/components/ui/collapsible' +import { Calendar, Loader2, ChevronDown, MapPin } from 'lucide-vue-next' import { toastService } from '@/core/services/ToastService' import { injectService, SERVICE_TOKENS } from '@/core/di-container' -import ImageUpload from '@/modules/base/components/ImageUpload.vue' -import DatePicker from '@/modules/base/components/DatePicker.vue' -import TimePicker from '@/modules/base/components/TimePicker.vue' -import type { ImageUploadService, UploadedImage } from '@/modules/base/services/ImageUploadService' import type { TicketApiService } from '../services/TicketApiService' import type { CreateEventRequest } from '../types/ticket' import { ALL_CATEGORIES } from '../types/category' @@ -56,45 +58,17 @@ const emit = defineEmits<{ const { t } = useI18n() -// Fold a date input ("YYYY-MM-DD") and an optional time input ("HH:MM") -// into the events-extension wire format: date-only when no time given, -// ISO 8601 datetime otherwise. The publisher switches NIP-52 kinds on -// the "T" delimiter. Hoisted above the schema so the validation refine -// can reuse it. -function foldDateTime(date: string, time: string): string { - if (!date) return '' - return time ? `${date}T${time}` : date -} - -const formSchema = toTypedSchema( - z - .object({ - name: z.string().min(1, "Title is required").max(200, "Title too long"), - info: z.string().max(2000, "Description too long").optional().default(''), - event_start_date: z.string().min(1, "Start date is required"), - event_start_time: z.string().optional().default(''), - event_end_date: z.string().optional().default(''), - event_end_time: z.string().optional().default(''), - location: z.string().max(500).optional().default(''), - currency: z.string().default("sat"), - amount_tickets: z.number().min(0).max(100000).default(0), - price_per_ticket: z.number().min(0).default(0), - }) - .superRefine((v, ctx) => { - // End must not precede start. Compare on the folded date+time - // string so equal-date / later-time is enforced too. - if (!v.event_end_date) return - const start = foldDateTime(v.event_start_date, v.event_start_time) - const end = foldDateTime(v.event_end_date, v.event_end_time) - if (start && end && end < start) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - path: ['event_end_date'], - message: 'End must be on or after start', - }) - } - }) -) +const formSchema = toTypedSchema(z.object({ + name: z.string().min(1, "Title is required").max(200, "Title too long"), + info: z.string().max(2000, "Description too long").optional().default(''), + event_start_date: z.string().min(1, "Start date is required"), + event_end_date: z.string().optional().default(''), + location: z.string().max(500).optional().default(''), + banner: z.string().optional().default(''), + currency: z.string().default("sat"), + amount_tickets: z.number().min(0).max(100000).default(0), + price_per_ticket: z.number().min(0).default(0), +})) const form = useForm({ validationSchema: formSchema, @@ -102,44 +76,22 @@ const form = useForm({ name: '', info: '', event_start_date: '', - event_start_time: '', event_end_date: '', - event_end_time: '', location: '', + banner: '', currency: 'sat', amount_tickets: 0, price_per_ticket: 0, } }) -interface BannerImage extends UploadedImage { - isPrimary: boolean -} -const bannerImages = ref([]) - -// Auto-mirror end date to start: when the user picks a start date, -// surface that same date in the end-date picker so a one-day event -// requires no extra clicks. Don't overwrite an end date the user -// already set *after* the start — only fill when empty or when the -// existing end has fallen behind the new start. -watch( - () => form.values.event_start_date, - (start, prev) => { - if (!start) return - const end = form.values.event_end_date - if (!end || end < start || end === prev) { - form.setFieldValue('event_end_date', start) - } - } -) - const paymentService = injectService(SERVICE_TOKENS.PAYMENT_SERVICE) const ticketApi = injectService(SERVICE_TOKENS.TICKET_API) as TicketApiService | null -const imageService = injectService(SERVICE_TOKENS.IMAGE_UPLOAD_SERVICE) const availableCurrencies = ref(['sat']) const loadingCurrencies = ref(false) const selectedCategories = ref([]) +const showMoreOptions = ref(false) watch(() => props.open, async (isOpen) => { if (isOpen && ticketApi && !loadingCurrencies.value) { @@ -154,6 +106,7 @@ watch(() => props.open, async (isOpen) => { } if (!isOpen) { selectedCategories.value = [] + showMoreOptions.value = false } }) @@ -191,25 +144,15 @@ const onSubmit = form.handleSubmit(async (formValues) => { try { const eventData: CreateEventRequest = { name: formValues.name, - event_start_date: foldDateTime( - formValues.event_start_date, - formValues.event_start_time - ), + event_start_date: formValues.event_start_date, wallet: preferredWallet.id, } // Optional fields — only include if provided if (formValues.info) eventData.info = formValues.info - if (formValues.event_end_date) { - eventData.event_end_date = foldDateTime( - formValues.event_end_date, - formValues.event_end_time - ) - } + if (formValues.event_end_date) eventData.event_end_date = formValues.event_end_date if (formValues.location) eventData.location = formValues.location - if (bannerImages.value.length > 0) { - eventData.banner = imageService.getImageUrl(bannerImages.value[0].alias) - } + if (formValues.banner) eventData.banner = formValues.banner if (formValues.currency) eventData.currency = formValues.currency if (formValues.amount_tickets) eventData.amount_tickets = formValues.amount_tickets if (formValues.price_per_ticket) eventData.price_per_ticket = formValues.price_per_ticket @@ -219,7 +162,6 @@ const onSubmit = form.handleSubmit(async (formValues) => { toastService.success('Event submitted!') resetForm() selectedCategories.value = [] - bannerImages.value = [] emit('update:open', false) emit('event-created') } catch (error) { @@ -234,7 +176,6 @@ const handleOpenChange = (open: boolean) => { if (!open && !isLoading.value) { resetForm() selectedCategories.value = [] - bannerImages.value = [] } emit('update:open', open) } @@ -266,74 +207,16 @@ const handleOpenChange = (open: boolean) => { - - - - - Start date * - - - - - - - - - - Start time - - - - - - - - - - - - - End date - - - - - - - - - - - End time - (optional) - - - - - - - - + + + + Start date * + + + + + + @@ -380,26 +263,16 @@ const handleOpenChange = (open: boolean) => { - - - Banner image - - One poster image. Auto-resized to 1920px max edge and re-encoded as WebP. - - - + + + + Image URL + + + + + + @@ -445,6 +318,30 @@ const handleOpenChange = (open: boolean) => { + + + + + More options + + + + + + + + + End date + + + + Defaults to start date if not set + + + + + + diff --git a/src/modules/activities/types/nip52.ts b/src/modules/activities/types/nip52.ts index 332d054..2a57a71 100644 --- a/src/modules/activities/types/nip52.ts +++ b/src/modules/activities/types/nip52.ts @@ -181,14 +181,6 @@ export function parseCalendarDateEvent(event: NostrEvent): CalendarDateEvent | n if (!dTag || !title || !start) return null - // NIP-52 kind 31922 requires YYYY-MM-DD. Reject anything else (including - // accidentally-published datetimes) so downstream parseIsoDate cannot - // produce an Invalid Date and crash the renderer. - const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/ - if (!ISO_DATE.test(start)) return null - const end = getTagValue(event.tags, 'end') - if (end && !ISO_DATE.test(end)) return null - const participants: Participant[] = event.tags .filter(t => t[0] === 'p') .map(t => ({ @@ -205,7 +197,7 @@ export function parseCalendarDateEvent(event: NostrEvent): CalendarDateEvent | n content: event.content, image: getTagValue(event.tags, 'image'), start, - end, + end: getTagValue(event.tags, 'end'), location: getTagValue(event.tags, 'location'), geohash: getTagValue(event.tags, 'g'), hashtags: getTagValues(event.tags, 't'), diff --git a/src/modules/activities/types/ticket.ts b/src/modules/activities/types/ticket.ts index 5384ddf..8943421 100644 --- a/src/modules/activities/types/ticket.ts +++ b/src/modules/activities/types/ticket.ts @@ -44,10 +44,6 @@ export interface TicketPaymentStatus { /** * LNbits events extension event (database-backed ticketed event). * Corresponds to the Event model in the events extension. - * - * event_start_date / event_end_date are ISO 8601 — either date-only - * ("2026-05-19") or with a time ("2026-05-19T18:30"). Presence of "T" - * switches the publisher between NIP-52 kind 31922 and 31923. */ export interface TicketedEvent { id: string diff --git a/src/modules/activities/views/EventsPage.vue b/src/modules/activities/views/EventsPage.vue index ab4feb8..0181dc1 100644 --- a/src/modules/activities/views/EventsPage.vue +++ b/src/modules/activities/views/EventsPage.vue @@ -33,10 +33,7 @@ function formatDate(dateStr: string | null | undefined) { if (!dateStr) return 'Date not available' const date = new Date(dateStr) if (isNaN(date.getTime())) return 'Invalid date' - // Presence of "T" in the wire value marks a time-based event (NIP-52 - // kind 31923 on our publisher). Show time only when one was set. - const hasTime = dateStr.includes('T') - return format(date, hasTime ? 'MMMM do, yyyy p' : 'MMMM do, yyyy') + return format(date, 'MMMM do, yyyy') } function handlePurchaseClick(event: { diff --git a/src/modules/base/components/DatePicker.vue b/src/modules/base/components/DatePicker.vue deleted file mode 100644 index e639045..0000000 --- a/src/modules/base/components/DatePicker.vue +++ /dev/null @@ -1,98 +0,0 @@ - - - - - - - - {{ display || placeholder || 'Pick a date' }} - - - - - - - - - - diff --git a/src/modules/base/components/ImageUpload.vue b/src/modules/base/components/ImageUpload.vue index 71f96af..d85817f 100644 --- a/src/modules/base/components/ImageUpload.vue +++ b/src/modules/base/components/ImageUpload.vue @@ -160,7 +160,7 @@ import { Camera, X, Loader2, AlertCircle, Image } from 'lucide-vue-next' import { Button } from '@/components/ui/button' import { Alert, AlertDescription } from '@/components/ui/alert' import { injectService, SERVICE_TOKENS } from '@/core/di-container' -import type { ImageUploadService, UploadedImage, CompressOptions } from '../services/ImageUploadService' +import type { ImageUploadService, UploadedImage } from '../services/ImageUploadService' interface ImageWithMetadata extends UploadedImage { isPrimary: boolean @@ -175,12 +175,6 @@ const props = defineProps<{ disabled?: boolean placeholder?: string allowCamera?: boolean - /** - * Client-side resize + re-encode before upload. Pass `true` for the - * service defaults (1920px max edge, WebP, ~1MB target), or an object - * to tune individual knobs. - */ - compress?: boolean | CompressOptions }>() const emit = defineEmits<{ @@ -385,8 +379,7 @@ const uploadFiles = async (files: File[]) => { try { const uploadOptions = { - maxSizeMB: maxSizeMB.value, - compress: props.compress, + maxSizeMB: maxSizeMB.value } // Upload files with better error handling diff --git a/src/modules/base/components/TimePicker.vue b/src/modules/base/components/TimePicker.vue deleted file mode 100644 index cb97ac7..0000000 --- a/src/modules/base/components/TimePicker.vue +++ /dev/null @@ -1,106 +0,0 @@ - - - - - - - - - - - - - {{ h }} - - - - - : - - - - - - - - {{ m }} - - - - - - - - - diff --git a/src/modules/base/services/ImageUploadService.ts b/src/modules/base/services/ImageUploadService.ts index d045a11..cabe234 100644 --- a/src/modules/base/services/ImageUploadService.ts +++ b/src/modules/base/services/ImageUploadService.ts @@ -1,4 +1,3 @@ -import imageCompression from 'browser-image-compression' import { BaseService } from '@/core/base/BaseService' import type { ServiceMetadata } from '@/core/base/BaseService' import appConfig from '@/app.config' @@ -14,36 +13,10 @@ export interface UploadedImage { } } -/** - * Client-side compression knobs. Resize first, then re-encode. - * - * Defaults target a typical banner/poster: longest edge 1920px, WebP - * at q=0.85, aim for ~1MB output. EXIF orientation is handled by the - * library (canvas drawImage doesn't auto-rotate, which is the classic - * "portrait photo lands sideways" bug). - */ -export interface CompressOptions { - maxSizeMB?: number - maxWidthOrHeight?: number - initialQuality?: number - fileType?: 'image/webp' | 'image/jpeg' - useWebWorker?: boolean -} - export interface ImageUploadOptions { maxSizeMB?: number acceptedTypes?: string[] generateThumbnail?: boolean - /** Enable client-side resize + re-encode before upload. */ - compress?: boolean | CompressOptions -} - -const DEFAULT_COMPRESS: Required = { - maxSizeMB: 1, - maxWidthOrHeight: 1920, - initialQuality: 0.85, - fileType: 'image/webp', - useWebWorker: true, } export interface ImageUrlOptions { @@ -103,12 +76,8 @@ export class ImageUploadService extends BaseService { // Validate file this.validateFile(file, options) - // Optional client-side resize + re-encode. Keeps phone photos - // (5–8MB originals) from hitting pict-rs as full-resolution files. - const fileToUpload = await this.maybeCompress(file, options) - const formData = new FormData() - formData.append('images[]', fileToUpload) + formData.append('images[]', file) const response = await fetch(`${this.baseUrl}/image`, { method: 'POST', @@ -322,50 +291,6 @@ export class ImageUploadService extends BaseService { return alias } - /** - * Resize + re-encode the file when compression is requested. Returns - * the original file untouched when compression is off, the file is - * already smaller than the target, or when re-encoding produces a - * larger blob than the input (rare but possible on already-optimized - * sources). - * - * The library handles EXIF orientation internally and falls back to - * JPEG if the browser can't encode WebP. - */ - private async maybeCompress(file: File, options: ImageUploadOptions): Promise { - if (!options.compress) return file - - const opts: Required = { - ...DEFAULT_COMPRESS, - ...(typeof options.compress === 'object' ? options.compress : {}), - } - - // Skip if the source is already smaller than the target and within - // dimension limits (we can't cheaply check dimensions without - // decoding, so size-only is the pragmatic short-circuit). - if (file.size <= opts.maxSizeMB * 1024 * 1024 * 0.6) { - this.debug(`Skipping compression: ${file.name} already ${(file.size / 1024 / 1024).toFixed(2)}MB`) - return file - } - - try { - const compressed = await imageCompression(file, opts) - if (compressed.size >= file.size) { - this.debug(`Compression made ${file.name} larger; keeping original`) - return file - } - this.debug( - `Compressed ${file.name}: ${(file.size / 1024 / 1024).toFixed(2)}MB → ${(compressed.size / 1024 / 1024).toFixed(2)}MB` - ) - return compressed - } catch (err) { - // HEIC / encoder edge cases — fall back to the original file so the - // upload still succeeds. Server-side processing handles delivery. - this.debug('Compression failed, uploading original:', err) - return file - } - } - /** * Validate file before upload */
Banner image
- One poster image. Auto-resized to 1920px max edge and re-encoded as WebP. -