Compare commits
No commits in common. "260949d3bfe836f7214342fe49f582b8b40e2f50" and "49c75e4d2f8eae1b725dfd6d2e161cd395a20bb9" have entirely different histories.
260949d3bf
...
49c75e4d2f
23 changed files with 108 additions and 899 deletions
134
package-lock.json
generated
134
package-lock.json
generated
|
|
@ -8,13 +8,11 @@
|
||||||
"name": "aio-shadcn-vite",
|
"name": "aio-shadcn-vite",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@internationalized/date": "^3.12.1",
|
|
||||||
"@tanstack/vue-table": "^8.21.3",
|
"@tanstack/vue-table": "^8.21.3",
|
||||||
"@vee-validate/zod": "^4.15.1",
|
"@vee-validate/zod": "^4.15.1",
|
||||||
"@vueuse/components": "^12.5.0",
|
"@vueuse/components": "^12.5.0",
|
||||||
"@vueuse/core": "^12.8.2",
|
"@vueuse/core": "^12.8.2",
|
||||||
"@vueuse/integrations": "^13.6.0",
|
"@vueuse/integrations": "^13.6.0",
|
||||||
"browser-image-compression": "^2.0.2",
|
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
|
|
@ -29,7 +27,7 @@
|
||||||
"qr-scanner": "^1.4.2",
|
"qr-scanner": "^1.4.2",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"radix-vue": "^1.9.13",
|
"radix-vue": "^1.9.13",
|
||||||
"reka-ui": "^2.9.7",
|
"reka-ui": "^2.6.0",
|
||||||
"tailwind-merge": "^2.6.0",
|
"tailwind-merge": "^2.6.0",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"unique-names-generator": "^4.7.1",
|
"unique-names-generator": "^4.7.1",
|
||||||
|
|
@ -147,6 +145,7 @@
|
||||||
"integrity": "sha512-l+lkXCHS6tQEc5oUpK28xBOZ6+HwaH7YwoYQbLFiYb4nS2/l1tKnZEtEWkD0GuiYdvArf9qBS0XlQGXzPMsNqQ==",
|
"integrity": "sha512-l+lkXCHS6tQEc5oUpK28xBOZ6+HwaH7YwoYQbLFiYb4nS2/l1tKnZEtEWkD0GuiYdvArf9qBS0XlQGXzPMsNqQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ampproject/remapping": "^2.2.0",
|
"@ampproject/remapping": "^2.2.0",
|
||||||
"@babel/code-frame": "^7.26.2",
|
"@babel/code-frame": "^7.26.2",
|
||||||
|
|
@ -2652,6 +2651,7 @@
|
||||||
"integrity": "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==",
|
"integrity": "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chalk": "^4.1.1",
|
"chalk": "^4.1.1",
|
||||||
"fs-extra": "^9.0.1",
|
"fs-extra": "^9.0.1",
|
||||||
|
|
@ -3949,9 +3949,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@internationalized/date": {
|
"node_modules/@internationalized/date": {
|
||||||
"version": "3.12.1",
|
"version": "3.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.12.1.tgz",
|
"resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.7.0.tgz",
|
||||||
"integrity": "sha512-6IedsVWXyq4P9Tj+TxuU8WGWM70hYLl12nbYU8jkikVpa6WXapFazPUcHUMDMoWftIDE2ILDkFFte6W2nFCkRQ==",
|
"integrity": "sha512-VJ5WS3fcVx0bejE/YHfbDKR/yawZgKqn/if+oEeLqNwBtPzVB06olkfcnojTmEMX+gTpH+FlQ69SHNitJ8/erQ==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@swc/helpers": "^0.5.0"
|
"@swc/helpers": "^0.5.0"
|
||||||
|
|
@ -5718,6 +5718,7 @@
|
||||||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-deep-equal": "^3.1.3",
|
"fast-deep-equal": "^3.1.3",
|
||||||
"fast-uri": "^3.0.1",
|
"fast-uri": "^3.0.1",
|
||||||
|
|
@ -6060,15 +6061,6 @@
|
||||||
"node": ">=8"
|
"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": {
|
"node_modules/browserslist": {
|
||||||
"version": "4.24.4",
|
"version": "4.24.4",
|
||||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz",
|
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz",
|
||||||
|
|
@ -6089,6 +6081,7 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"caniuse-lite": "^1.0.30001688",
|
"caniuse-lite": "^1.0.30001688",
|
||||||
"electron-to-chromium": "^1.5.73",
|
"electron-to-chromium": "^1.5.73",
|
||||||
|
|
@ -7055,9 +7048,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/defu": {
|
"node_modules/defu": {
|
||||||
"version": "6.1.7",
|
"version": "6.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
|
||||||
"integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==",
|
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/detect-libc": {
|
"node_modules/detect-libc": {
|
||||||
|
|
@ -7635,17 +7628,6 @@
|
||||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/end-of-stream": {
|
||||||
"version": "1.4.4",
|
"version": "1.4.4",
|
||||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.0.0.tgz",
|
||||||
"integrity": "sha512-14F4hBIxqKvD4Zz/XjDc3y94mNZN6pRv3U13Udo0lNLCWRBUsrMv2xwcF/y/Z5sV6+FQW+/ow68cHpm4sunt8Q==",
|
"integrity": "sha512-14F4hBIxqKvD4Zz/XjDc3y94mNZN6pRv3U13Udo0lNLCWRBUsrMv2xwcF/y/Z5sV6+FQW+/ow68cHpm4sunt8Q==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
|
|
@ -8935,20 +8918,6 @@
|
||||||
"ms": "^2.0.0"
|
"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": {
|
"node_modules/idb": {
|
||||||
"version": "7.1.1",
|
"version": "7.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
|
||||||
"integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
|
"integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dijkstrajs": "^1.0.1",
|
"dijkstrajs": "^1.0.1",
|
||||||
"pngjs": "^5.0.0",
|
"pngjs": "^5.0.0",
|
||||||
|
|
@ -12226,9 +12196,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/reka-ui": {
|
"node_modules/reka-ui": {
|
||||||
"version": "2.9.7",
|
"version": "2.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/reka-ui/-/reka-ui-2.9.7.tgz",
|
"resolved": "https://registry.npmjs.org/reka-ui/-/reka-ui-2.6.0.tgz",
|
||||||
"integrity": "sha512-aX7foYYR20v4+majO58OJJdBNfLMm0eJb448l9N4JVy8JB7GXOr4H/S4a+J1pkcoxZH8Cb7YHpJ855+miAm7sA==",
|
"integrity": "sha512-NrGMKrABD97l890mFS3TNUzB0BLUfbL3hh0NjcJRIUSUljb288bx3Mzo31nOyUcdiiW0HqFGXJwyCBh9cWgb0w==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@floating-ui/dom": "^1.6.13",
|
"@floating-ui/dom": "^1.6.13",
|
||||||
|
|
@ -12236,62 +12206,26 @@
|
||||||
"@internationalized/date": "^3.5.0",
|
"@internationalized/date": "^3.5.0",
|
||||||
"@internationalized/number": "^3.5.0",
|
"@internationalized/number": "^3.5.0",
|
||||||
"@tanstack/vue-virtual": "^3.12.0",
|
"@tanstack/vue-virtual": "^3.12.0",
|
||||||
"@vueuse/core": "^14.1.0",
|
"@vueuse/core": "^12.5.0",
|
||||||
"@vueuse/shared": "^14.1.0",
|
"@vueuse/shared": "^12.5.0",
|
||||||
"aria-hidden": "^1.2.4",
|
"aria-hidden": "^1.2.4",
|
||||||
"defu": "^6.1.5",
|
"defu": "^6.1.4",
|
||||||
"ohash": "^2.0.11"
|
"ohash": "^2.0.11"
|
||||||
},
|
},
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/zernonia"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"vue": ">= 3.4.0"
|
"vue": ">= 3.2.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"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/reka-ui/node_modules/@vueuse/shared": {
|
"node_modules/reka-ui/node_modules/@vueuse/shared": {
|
||||||
"version": "14.3.0",
|
"version": "12.8.2",
|
||||||
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-12.8.2.tgz",
|
||||||
"integrity": "sha512-bZpge9eSXwa4ToSiqJ7j6KRwhAsneMFoSz3LMWKQDkqimm3D/tbFlrklrs/IOqC8tEcYmXQZJ6N0UrjhBirVCg==",
|
"integrity": "sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"vue": "^3.5.13"
|
||||||
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/antfu"
|
"url": "https://github.com/sponsors/antfu"
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"vue": "^3.5.0"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/require-directory": {
|
"node_modules/require-directory": {
|
||||||
|
|
@ -12443,6 +12377,7 @@
|
||||||
"integrity": "sha512-4iya7Jb76fVpQyLoiVpzUrsjQ12r3dM7fIVz+4NwoYvZOShknRmiv+iu9CClZml5ZLGb0XMcYLutK6w9tgxHDw==",
|
"integrity": "sha512-4iya7Jb76fVpQyLoiVpzUrsjQ12r3dM7fIVz+4NwoYvZOShknRmiv+iu9CClZml5ZLGb0XMcYLutK6w9tgxHDw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/estree": "1.0.8"
|
"@types/estree": "1.0.8"
|
||||||
},
|
},
|
||||||
|
|
@ -13452,7 +13387,8 @@
|
||||||
"version": "4.0.12",
|
"version": "4.0.12",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.12.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.12.tgz",
|
||||||
"integrity": "sha512-bT0hJo91FtncsAMSsMzUkoo/iEU0Xs5xgFgVC9XmdM9bw5MhZuQFjPNl6wxAE0SiQF/YTZJa+PndGWYSDtuxAg==",
|
"integrity": "sha512-bT0hJo91FtncsAMSsMzUkoo/iEU0Xs5xgFgVC9XmdM9bw5MhZuQFjPNl6wxAE0SiQF/YTZJa+PndGWYSDtuxAg==",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/tailwindcss-animate": {
|
"node_modules/tailwindcss-animate": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
|
|
@ -13587,6 +13523,7 @@
|
||||||
"integrity": "sha512-GWANVlPM/ZfYzuPHjq0nxT+EbOEDDN3Jwhwdg1D8TU8oSkktp8w64Uq4auuGLxFSoNTRDncTq2hQHX1Ld9KHkA==",
|
"integrity": "sha512-GWANVlPM/ZfYzuPHjq0nxT+EbOEDDN3Jwhwdg1D8TU8oSkktp8w64Uq4auuGLxFSoNTRDncTq2hQHX1Ld9KHkA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/source-map": "^0.3.3",
|
"@jridgewell/source-map": "^0.3.3",
|
||||||
"acorn": "^8.8.2",
|
"acorn": "^8.8.2",
|
||||||
|
|
@ -13816,6 +13753,7 @@
|
||||||
"integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
|
"integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
|
|
@ -14015,12 +13953,6 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/validate-npm-package-license": {
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
|
"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==",
|
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
"fdir": "^6.4.4",
|
"fdir": "^6.4.4",
|
||||||
|
|
@ -14296,6 +14229,7 @@
|
||||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz",
|
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz",
|
||||||
"integrity": "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==",
|
"integrity": "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/compiler-dom": "3.5.13",
|
"@vue/compiler-dom": "3.5.13",
|
||||||
"@vue/compiler-sfc": "3.5.13",
|
"@vue/compiler-sfc": "3.5.13",
|
||||||
|
|
@ -14748,6 +14682,7 @@
|
||||||
"integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==",
|
"integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"rollup": "dist/bin/rollup"
|
"rollup": "dist/bin/rollup"
|
||||||
},
|
},
|
||||||
|
|
@ -15005,6 +14940,7 @@
|
||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,13 +43,11 @@
|
||||||
"make": "electron-forge make"
|
"make": "electron-forge make"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@internationalized/date": "^3.12.1",
|
|
||||||
"@tanstack/vue-table": "^8.21.3",
|
"@tanstack/vue-table": "^8.21.3",
|
||||||
"@vee-validate/zod": "^4.15.1",
|
"@vee-validate/zod": "^4.15.1",
|
||||||
"@vueuse/components": "^12.5.0",
|
"@vueuse/components": "^12.5.0",
|
||||||
"@vueuse/core": "^12.8.2",
|
"@vueuse/core": "^12.8.2",
|
||||||
"@vueuse/integrations": "^13.6.0",
|
"@vueuse/integrations": "^13.6.0",
|
||||||
"browser-image-compression": "^2.0.2",
|
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
|
|
@ -64,7 +62,7 @@
|
||||||
"qr-scanner": "^1.4.2",
|
"qr-scanner": "^1.4.2",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"radix-vue": "^1.9.13",
|
"radix-vue": "^1.9.13",
|
||||||
"reka-ui": "^2.9.7",
|
"reka-ui": "^2.6.0",
|
||||||
"tailwind-merge": "^2.6.0",
|
"tailwind-merge": "^2.6.0",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"unique-names-generator": "^4.7.1",
|
"unique-names-generator": "^4.7.1",
|
||||||
|
|
|
||||||
|
|
@ -1,58 +0,0 @@
|
||||||
<script lang="ts" setup>
|
|
||||||
import type { CalendarRootEmits, CalendarRootProps } from "reka-ui"
|
|
||||||
import type { HTMLAttributes } from "vue"
|
|
||||||
import { reactiveOmit } from "@vueuse/core"
|
|
||||||
import { CalendarRoot, useForwardPropsEmits } from "reka-ui"
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
import { CalendarCell, CalendarCellTrigger, CalendarGrid, CalendarGridBody, CalendarGridHead, CalendarGridRow, CalendarHeadCell, CalendarHeader, CalendarHeading, CalendarNextButton, CalendarPrevButton } from "."
|
|
||||||
|
|
||||||
const props = defineProps<CalendarRootProps & { class?: HTMLAttributes["class"] }>()
|
|
||||||
|
|
||||||
const emits = defineEmits<CalendarRootEmits>()
|
|
||||||
|
|
||||||
const delegatedProps = reactiveOmit(props, "class")
|
|
||||||
|
|
||||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<CalendarRoot
|
|
||||||
v-slot="{ grid, weekDays }"
|
|
||||||
:class="cn('p-3', props.class)"
|
|
||||||
v-bind="forwarded"
|
|
||||||
>
|
|
||||||
<CalendarHeader>
|
|
||||||
<CalendarPrevButton />
|
|
||||||
<CalendarHeading />
|
|
||||||
<CalendarNextButton />
|
|
||||||
</CalendarHeader>
|
|
||||||
|
|
||||||
<div class="flex flex-col gap-y-4 mt-4 sm:flex-row sm:gap-x-4 sm:gap-y-0">
|
|
||||||
<CalendarGrid v-for="month in grid" :key="month.value.toString()">
|
|
||||||
<CalendarGridHead>
|
|
||||||
<CalendarGridRow>
|
|
||||||
<CalendarHeadCell
|
|
||||||
v-for="day in weekDays" :key="day"
|
|
||||||
>
|
|
||||||
{{ day }}
|
|
||||||
</CalendarHeadCell>
|
|
||||||
</CalendarGridRow>
|
|
||||||
</CalendarGridHead>
|
|
||||||
<CalendarGridBody>
|
|
||||||
<CalendarGridRow v-for="(weekDates, index) in month.rows" :key="`weekDate-${index}`" class="mt-2 w-full">
|
|
||||||
<CalendarCell
|
|
||||||
v-for="weekDate in weekDates"
|
|
||||||
:key="weekDate.toString()"
|
|
||||||
:date="weekDate"
|
|
||||||
>
|
|
||||||
<CalendarCellTrigger
|
|
||||||
:day="weekDate"
|
|
||||||
:month="month.value"
|
|
||||||
/>
|
|
||||||
</CalendarCell>
|
|
||||||
</CalendarGridRow>
|
|
||||||
</CalendarGridBody>
|
|
||||||
</CalendarGrid>
|
|
||||||
</div>
|
|
||||||
</CalendarRoot>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
<script lang="ts" setup>
|
|
||||||
import type { CalendarCellProps } from "reka-ui"
|
|
||||||
import type { HTMLAttributes } from "vue"
|
|
||||||
import { reactiveOmit } from "@vueuse/core"
|
|
||||||
import { CalendarCell, useForwardProps } from "reka-ui"
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const props = defineProps<CalendarCellProps & { class?: HTMLAttributes["class"] }>()
|
|
||||||
|
|
||||||
const delegatedProps = reactiveOmit(props, "class")
|
|
||||||
|
|
||||||
const forwardedProps = useForwardProps(delegatedProps)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<CalendarCell
|
|
||||||
:class="cn('relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([data-selected])]:rounded-md [&:has([data-selected])]:bg-accent [&:has([data-selected][data-outside-view])]:bg-accent/50', props.class)"
|
|
||||||
v-bind="forwardedProps"
|
|
||||||
>
|
|
||||||
<slot />
|
|
||||||
</CalendarCell>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
<script lang="ts" setup>
|
|
||||||
import type { CalendarCellTriggerProps } from "reka-ui"
|
|
||||||
import type { HTMLAttributes } from "vue"
|
|
||||||
import { reactiveOmit } from "@vueuse/core"
|
|
||||||
import { CalendarCellTrigger, useForwardProps } from "reka-ui"
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
import { buttonVariants } from '@/components/ui/button'
|
|
||||||
|
|
||||||
const props = defineProps<CalendarCellTriggerProps & { class?: HTMLAttributes["class"] }>()
|
|
||||||
|
|
||||||
const delegatedProps = reactiveOmit(props, "class")
|
|
||||||
|
|
||||||
const forwardedProps = useForwardProps(delegatedProps)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<CalendarCellTrigger
|
|
||||||
:class="cn(
|
|
||||||
buttonVariants({ variant: 'ghost' }),
|
|
||||||
'h-8 w-8 p-0 font-normal',
|
|
||||||
'[&[data-today]:not([data-selected])]:bg-accent [&[data-today]:not([data-selected])]:text-accent-foreground',
|
|
||||||
// Selected
|
|
||||||
'data-[selected]:bg-primary data-[selected]:text-primary-foreground data-[selected]:opacity-100 data-[selected]:hover:bg-primary data-[selected]:hover:text-primary-foreground data-[selected]:focus:bg-primary data-[selected]:focus:text-primary-foreground',
|
|
||||||
// Disabled
|
|
||||||
'data-[disabled]:text-muted-foreground data-[disabled]:opacity-50',
|
|
||||||
// Unavailable
|
|
||||||
'data-[unavailable]:text-destructive-foreground data-[unavailable]:line-through',
|
|
||||||
// Outside months
|
|
||||||
'data-[outside-view]:text-muted-foreground data-[outside-view]:opacity-50 [&[data-outside-view][data-selected]]:bg-accent/50 [&[data-outside-view][data-selected]]:text-muted-foreground [&[data-outside-view][data-selected]]:opacity-30',
|
|
||||||
props.class,
|
|
||||||
)"
|
|
||||||
v-bind="forwardedProps"
|
|
||||||
>
|
|
||||||
<slot />
|
|
||||||
</CalendarCellTrigger>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
<script lang="ts" setup>
|
|
||||||
import type { CalendarGridProps } from "reka-ui"
|
|
||||||
import type { HTMLAttributes } from "vue"
|
|
||||||
import { reactiveOmit } from "@vueuse/core"
|
|
||||||
import { CalendarGrid, useForwardProps } from "reka-ui"
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const props = defineProps<CalendarGridProps & { class?: HTMLAttributes["class"] }>()
|
|
||||||
|
|
||||||
const delegatedProps = reactiveOmit(props, "class")
|
|
||||||
|
|
||||||
const forwardedProps = useForwardProps(delegatedProps)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<CalendarGrid
|
|
||||||
:class="cn('w-full border-collapse space-y-1', props.class)"
|
|
||||||
v-bind="forwardedProps"
|
|
||||||
>
|
|
||||||
<slot />
|
|
||||||
</CalendarGrid>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
<script lang="ts" setup>
|
|
||||||
import type { CalendarGridBodyProps } from "reka-ui"
|
|
||||||
import { CalendarGridBody } from "reka-ui"
|
|
||||||
|
|
||||||
const props = defineProps<CalendarGridBodyProps>()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<CalendarGridBody v-bind="props">
|
|
||||||
<slot />
|
|
||||||
</CalendarGridBody>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
<script lang="ts" setup>
|
|
||||||
import type { CalendarGridHeadProps } from "reka-ui"
|
|
||||||
import type { HTMLAttributes } from "vue"
|
|
||||||
import { CalendarGridHead } from "reka-ui"
|
|
||||||
|
|
||||||
const props = defineProps<CalendarGridHeadProps & { class?: HTMLAttributes["class"] }>()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<CalendarGridHead v-bind="props">
|
|
||||||
<slot />
|
|
||||||
</CalendarGridHead>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
<script lang="ts" setup>
|
|
||||||
import type { CalendarGridRowProps } from "reka-ui"
|
|
||||||
import type { HTMLAttributes } from "vue"
|
|
||||||
import { reactiveOmit } from "@vueuse/core"
|
|
||||||
import { CalendarGridRow, useForwardProps } from "reka-ui"
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const props = defineProps<CalendarGridRowProps & { class?: HTMLAttributes["class"] }>()
|
|
||||||
|
|
||||||
const delegatedProps = reactiveOmit(props, "class")
|
|
||||||
|
|
||||||
const forwardedProps = useForwardProps(delegatedProps)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<CalendarGridRow :class="cn('flex', props.class)" v-bind="forwardedProps">
|
|
||||||
<slot />
|
|
||||||
</CalendarGridRow>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
<script lang="ts" setup>
|
|
||||||
import type { CalendarHeadCellProps } from "reka-ui"
|
|
||||||
import type { HTMLAttributes } from "vue"
|
|
||||||
import { reactiveOmit } from "@vueuse/core"
|
|
||||||
import { CalendarHeadCell, useForwardProps } from "reka-ui"
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const props = defineProps<CalendarHeadCellProps & { class?: HTMLAttributes["class"] }>()
|
|
||||||
|
|
||||||
const delegatedProps = reactiveOmit(props, "class")
|
|
||||||
|
|
||||||
const forwardedProps = useForwardProps(delegatedProps)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<CalendarHeadCell :class="cn('w-8 rounded-md text-[0.8rem] font-normal text-muted-foreground', props.class)" v-bind="forwardedProps">
|
|
||||||
<slot />
|
|
||||||
</CalendarHeadCell>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
<script lang="ts" setup>
|
|
||||||
import type { CalendarHeaderProps } from "reka-ui"
|
|
||||||
import type { HTMLAttributes } from "vue"
|
|
||||||
import { reactiveOmit } from "@vueuse/core"
|
|
||||||
import { CalendarHeader, useForwardProps } from "reka-ui"
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const props = defineProps<CalendarHeaderProps & { class?: HTMLAttributes["class"] }>()
|
|
||||||
|
|
||||||
const delegatedProps = reactiveOmit(props, "class")
|
|
||||||
|
|
||||||
const forwardedProps = useForwardProps(delegatedProps)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<CalendarHeader :class="cn('relative flex w-full items-center justify-between pt-1', props.class)" v-bind="forwardedProps">
|
|
||||||
<slot />
|
|
||||||
</CalendarHeader>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
<script lang="ts" setup>
|
|
||||||
import type { CalendarHeadingProps } from "reka-ui"
|
|
||||||
import type { HTMLAttributes } from "vue"
|
|
||||||
import { reactiveOmit } from "@vueuse/core"
|
|
||||||
import { CalendarHeading, useForwardProps } from "reka-ui"
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const props = defineProps<CalendarHeadingProps & { class?: HTMLAttributes["class"] }>()
|
|
||||||
|
|
||||||
defineSlots<{
|
|
||||||
default: (props: { headingValue: string }) => any
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const delegatedProps = reactiveOmit(props, "class")
|
|
||||||
|
|
||||||
const forwardedProps = useForwardProps(delegatedProps)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<CalendarHeading
|
|
||||||
v-slot="{ headingValue }"
|
|
||||||
:class="cn('text-sm font-medium', props.class)"
|
|
||||||
v-bind="forwardedProps"
|
|
||||||
>
|
|
||||||
<slot :heading-value>
|
|
||||||
{{ headingValue }}
|
|
||||||
</slot>
|
|
||||||
</CalendarHeading>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
<script lang="ts" setup>
|
|
||||||
import type { CalendarNextProps } from "reka-ui"
|
|
||||||
import type { HTMLAttributes } from "vue"
|
|
||||||
import { reactiveOmit } from "@vueuse/core"
|
|
||||||
import { ChevronRight } from "lucide-vue-next"
|
|
||||||
import { CalendarNext, useForwardProps } from "reka-ui"
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
import { buttonVariants } from '@/components/ui/button'
|
|
||||||
|
|
||||||
const props = defineProps<CalendarNextProps & { class?: HTMLAttributes["class"] }>()
|
|
||||||
|
|
||||||
const delegatedProps = reactiveOmit(props, "class")
|
|
||||||
|
|
||||||
const forwardedProps = useForwardProps(delegatedProps)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<CalendarNext
|
|
||||||
:class="cn(
|
|
||||||
buttonVariants({ variant: 'outline' }),
|
|
||||||
'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100',
|
|
||||||
props.class,
|
|
||||||
)"
|
|
||||||
v-bind="forwardedProps"
|
|
||||||
>
|
|
||||||
<slot>
|
|
||||||
<ChevronRight class="h-4 w-4" />
|
|
||||||
</slot>
|
|
||||||
</CalendarNext>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
<script lang="ts" setup>
|
|
||||||
import type { CalendarPrevProps } from "reka-ui"
|
|
||||||
import type { HTMLAttributes } from "vue"
|
|
||||||
import { reactiveOmit } from "@vueuse/core"
|
|
||||||
import { ChevronLeft } from "lucide-vue-next"
|
|
||||||
import { CalendarPrev, useForwardProps } from "reka-ui"
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
import { buttonVariants } from '@/components/ui/button'
|
|
||||||
|
|
||||||
const props = defineProps<CalendarPrevProps & { class?: HTMLAttributes["class"] }>()
|
|
||||||
|
|
||||||
const delegatedProps = reactiveOmit(props, "class")
|
|
||||||
|
|
||||||
const forwardedProps = useForwardProps(delegatedProps)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<CalendarPrev
|
|
||||||
:class="cn(
|
|
||||||
buttonVariants({ variant: 'outline' }),
|
|
||||||
'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100',
|
|
||||||
props.class,
|
|
||||||
)"
|
|
||||||
v-bind="forwardedProps"
|
|
||||||
>
|
|
||||||
<slot>
|
|
||||||
<ChevronLeft class="h-4 w-4" />
|
|
||||||
</slot>
|
|
||||||
</CalendarPrev>
|
|
||||||
</template>
|
|
||||||
|
|
@ -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"
|
|
||||||
|
|
@ -24,6 +24,7 @@ import { Input } from '@/components/ui/input'
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
import { Separator } from '@/components/ui/separator'
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
|
|
@ -32,13 +33,14 @@ import {
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select'
|
} 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 { toastService } from '@/core/services/ToastService'
|
||||||
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
|
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 { TicketApiService } from '../services/TicketApiService'
|
||||||
import type { CreateEventRequest } from '../types/ticket'
|
import type { CreateEventRequest } from '../types/ticket'
|
||||||
import { ALL_CATEGORIES } from '../types/category'
|
import { ALL_CATEGORIES } from '../types/category'
|
||||||
|
|
@ -56,45 +58,17 @@ const emit = defineEmits<{
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
// Fold a date input ("YYYY-MM-DD") and an optional time input ("HH:MM")
|
const formSchema = toTypedSchema(z.object({
|
||||||
// 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"),
|
name: z.string().min(1, "Title is required").max(200, "Title too long"),
|
||||||
info: z.string().max(2000, "Description too long").optional().default(''),
|
info: z.string().max(2000, "Description too long").optional().default(''),
|
||||||
event_start_date: z.string().min(1, "Start date is required"),
|
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_date: z.string().optional().default(''),
|
||||||
event_end_time: z.string().optional().default(''),
|
|
||||||
location: z.string().max(500).optional().default(''),
|
location: z.string().max(500).optional().default(''),
|
||||||
|
banner: z.string().optional().default(''),
|
||||||
currency: z.string().default("sat"),
|
currency: z.string().default("sat"),
|
||||||
amount_tickets: z.number().min(0).max(100000).default(0),
|
amount_tickets: z.number().min(0).max(100000).default(0),
|
||||||
price_per_ticket: z.number().min(0).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 form = useForm({
|
const form = useForm({
|
||||||
validationSchema: formSchema,
|
validationSchema: formSchema,
|
||||||
|
|
@ -102,44 +76,22 @@ const form = useForm({
|
||||||
name: '',
|
name: '',
|
||||||
info: '',
|
info: '',
|
||||||
event_start_date: '',
|
event_start_date: '',
|
||||||
event_start_time: '',
|
|
||||||
event_end_date: '',
|
event_end_date: '',
|
||||||
event_end_time: '',
|
|
||||||
location: '',
|
location: '',
|
||||||
|
banner: '',
|
||||||
currency: 'sat',
|
currency: 'sat',
|
||||||
amount_tickets: 0,
|
amount_tickets: 0,
|
||||||
price_per_ticket: 0,
|
price_per_ticket: 0,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
interface BannerImage extends UploadedImage {
|
|
||||||
isPrimary: boolean
|
|
||||||
}
|
|
||||||
const bannerImages = ref<BannerImage[]>([])
|
|
||||||
|
|
||||||
// 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 paymentService = injectService(SERVICE_TOKENS.PAYMENT_SERVICE)
|
||||||
const ticketApi = injectService(SERVICE_TOKENS.TICKET_API) as TicketApiService | null
|
const ticketApi = injectService(SERVICE_TOKENS.TICKET_API) as TicketApiService | null
|
||||||
const imageService = injectService<ImageUploadService>(SERVICE_TOKENS.IMAGE_UPLOAD_SERVICE)
|
|
||||||
|
|
||||||
const availableCurrencies = ref<string[]>(['sat'])
|
const availableCurrencies = ref<string[]>(['sat'])
|
||||||
const loadingCurrencies = ref(false)
|
const loadingCurrencies = ref(false)
|
||||||
const selectedCategories = ref<string[]>([])
|
const selectedCategories = ref<string[]>([])
|
||||||
|
const showMoreOptions = ref(false)
|
||||||
|
|
||||||
watch(() => props.open, async (isOpen) => {
|
watch(() => props.open, async (isOpen) => {
|
||||||
if (isOpen && ticketApi && !loadingCurrencies.value) {
|
if (isOpen && ticketApi && !loadingCurrencies.value) {
|
||||||
|
|
@ -154,6 +106,7 @@ watch(() => props.open, async (isOpen) => {
|
||||||
}
|
}
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
selectedCategories.value = []
|
selectedCategories.value = []
|
||||||
|
showMoreOptions.value = false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -191,25 +144,15 @@ const onSubmit = form.handleSubmit(async (formValues) => {
|
||||||
try {
|
try {
|
||||||
const eventData: CreateEventRequest = {
|
const eventData: CreateEventRequest = {
|
||||||
name: formValues.name,
|
name: formValues.name,
|
||||||
event_start_date: foldDateTime(
|
event_start_date: formValues.event_start_date,
|
||||||
formValues.event_start_date,
|
|
||||||
formValues.event_start_time
|
|
||||||
),
|
|
||||||
wallet: preferredWallet.id,
|
wallet: preferredWallet.id,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optional fields — only include if provided
|
// Optional fields — only include if provided
|
||||||
if (formValues.info) eventData.info = formValues.info
|
if (formValues.info) eventData.info = formValues.info
|
||||||
if (formValues.event_end_date) {
|
if (formValues.event_end_date) eventData.event_end_date = formValues.event_end_date
|
||||||
eventData.event_end_date = foldDateTime(
|
|
||||||
formValues.event_end_date,
|
|
||||||
formValues.event_end_time
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (formValues.location) eventData.location = formValues.location
|
if (formValues.location) eventData.location = formValues.location
|
||||||
if (bannerImages.value.length > 0) {
|
if (formValues.banner) eventData.banner = formValues.banner
|
||||||
eventData.banner = imageService.getImageUrl(bannerImages.value[0].alias)
|
|
||||||
}
|
|
||||||
if (formValues.currency) eventData.currency = formValues.currency
|
if (formValues.currency) eventData.currency = formValues.currency
|
||||||
if (formValues.amount_tickets) eventData.amount_tickets = formValues.amount_tickets
|
if (formValues.amount_tickets) eventData.amount_tickets = formValues.amount_tickets
|
||||||
if (formValues.price_per_ticket) eventData.price_per_ticket = formValues.price_per_ticket
|
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!')
|
toastService.success('Event submitted!')
|
||||||
resetForm()
|
resetForm()
|
||||||
selectedCategories.value = []
|
selectedCategories.value = []
|
||||||
bannerImages.value = []
|
|
||||||
emit('update:open', false)
|
emit('update:open', false)
|
||||||
emit('event-created')
|
emit('event-created')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -234,7 +176,6 @@ const handleOpenChange = (open: boolean) => {
|
||||||
if (!open && !isLoading.value) {
|
if (!open && !isLoading.value) {
|
||||||
resetForm()
|
resetForm()
|
||||||
selectedCategories.value = []
|
selectedCategories.value = []
|
||||||
bannerImages.value = []
|
|
||||||
}
|
}
|
||||||
emit('update:open', open)
|
emit('update:open', open)
|
||||||
}
|
}
|
||||||
|
|
@ -266,75 +207,17 @@ const handleOpenChange = (open: boolean) => {
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<!-- Start date (required) + optional time -->
|
<!-- Start date (required) -->
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
<FormField v-slot="{ componentField }" name="event_start_date">
|
||||||
<FormField v-slot="{ value, handleChange }" name="event_start_date">
|
<FormItem>
|
||||||
<FormItem class="sm:col-span-2 min-w-0">
|
|
||||||
<FormLabel>Start date *</FormLabel>
|
<FormLabel>Start date *</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<DatePicker
|
<Input type="date" :min="today" v-bind="componentField" />
|
||||||
:model-value="value as string ?? ''"
|
|
||||||
:min="today"
|
|
||||||
placeholder="Pick a date"
|
|
||||||
@update:model-value="handleChange"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<FormField v-slot="{ value, handleChange }" name="event_start_time">
|
|
||||||
<FormItem class="min-w-0">
|
|
||||||
<FormLabel>Start time</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<TimePicker
|
|
||||||
:model-value="value as string ?? ''"
|
|
||||||
clearable
|
|
||||||
@update:model-value="handleChange"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
</FormField>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- End date + optional time. Auto-mirrors start date until
|
|
||||||
the user moves it forward; cross-field rule enforces
|
|
||||||
end >= start in the Zod schema. -->
|
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
|
||||||
<FormField v-slot="{ value, handleChange }" name="event_end_date">
|
|
||||||
<FormItem class="sm:col-span-2 min-w-0">
|
|
||||||
<FormLabel>End date</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<DatePicker
|
|
||||||
:model-value="value as string ?? ''"
|
|
||||||
:min="(form.values.event_start_date as string) || today"
|
|
||||||
placeholder="Pick a date"
|
|
||||||
@update:model-value="handleChange"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
<FormField v-slot="{ value, handleChange }" name="event_end_time">
|
|
||||||
<FormItem class="min-w-0">
|
|
||||||
<FormLabel>
|
|
||||||
End time
|
|
||||||
<span class="text-muted-foreground font-normal">(optional)</span>
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<TimePicker
|
|
||||||
:model-value="value as string ?? ''"
|
|
||||||
clearable
|
|
||||||
@update:model-value="handleChange"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
</FormField>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Description (optional, visible) -->
|
<!-- Description (optional, visible) -->
|
||||||
<FormField v-slot="{ componentField }" name="info">
|
<FormField v-slot="{ componentField }" name="info">
|
||||||
<FormItem>
|
<FormItem>
|
||||||
|
|
@ -380,26 +263,16 @@ const handleOpenChange = (open: boolean) => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Banner image (optional). Client-side compressed to ~1MB
|
<!-- Image URL (optional, visible) -->
|
||||||
WebP before upload to keep pict-rs storage in check.
|
<FormField v-slot="{ componentField }" name="banner">
|
||||||
Not a vee-validate field — managed via bannerImages ref. -->
|
<FormItem>
|
||||||
<div class="space-y-2">
|
<FormLabel>Image URL</FormLabel>
|
||||||
<p class="text-sm font-medium">Banner image</p>
|
<FormControl>
|
||||||
<p class="text-xs text-muted-foreground">
|
<Input type="url" placeholder="https://example.com/image.jpg" v-bind="componentField" />
|
||||||
One poster image. Auto-resized to 1920px max edge and re-encoded as WebP.
|
</FormControl>
|
||||||
</p>
|
<FormMessage />
|
||||||
<ImageUpload
|
</FormItem>
|
||||||
v-model="bannerImages"
|
</FormField>
|
||||||
:multiple="false"
|
|
||||||
:max-files="1"
|
|
||||||
:max-size-mb="10"
|
|
||||||
:show-primary-button="false"
|
|
||||||
:disabled="isLoading"
|
|
||||||
:allow-camera="true"
|
|
||||||
:compress="true"
|
|
||||||
placeholder="Add a poster or banner"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Tickets (optional, visible) -->
|
<!-- Tickets (optional, visible) -->
|
||||||
<div class="grid grid-cols-3 gap-3">
|
<div class="grid grid-cols-3 gap-3">
|
||||||
|
|
@ -445,6 +318,30 @@ const handleOpenChange = (open: boolean) => {
|
||||||
</FormField>
|
</FormField>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- More options (collapsible) -->
|
||||||
|
<Collapsible v-model:open="showMoreOptions">
|
||||||
|
<CollapsibleTrigger as-child>
|
||||||
|
<Button type="button" variant="ghost" size="sm" class="w-full justify-between text-muted-foreground">
|
||||||
|
More options
|
||||||
|
<ChevronDown class="w-4 h-4 transition-transform" :class="{ 'rotate-180': showMoreOptions }" />
|
||||||
|
</Button>
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
<CollapsibleContent class="space-y-4 pt-2">
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<FormField v-slot="{ componentField }" name="event_end_date">
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>End date</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input type="date" :min="today" v-bind="componentField" />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription class="text-xs">Defaults to start date if not set</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
</CollapsibleContent>
|
||||||
|
</Collapsible>
|
||||||
|
|
||||||
<!-- Actions -->
|
<!-- Actions -->
|
||||||
<div class="flex justify-end gap-3 pt-2">
|
<div class="flex justify-end gap-3 pt-2">
|
||||||
<Button type="button" variant="outline" @click="handleOpenChange(false)" :disabled="isLoading">
|
<Button type="button" variant="outline" @click="handleOpenChange(false)" :disabled="isLoading">
|
||||||
|
|
|
||||||
|
|
@ -181,14 +181,6 @@ export function parseCalendarDateEvent(event: NostrEvent): CalendarDateEvent | n
|
||||||
|
|
||||||
if (!dTag || !title || !start) return null
|
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
|
const participants: Participant[] = event.tags
|
||||||
.filter(t => t[0] === 'p')
|
.filter(t => t[0] === 'p')
|
||||||
.map(t => ({
|
.map(t => ({
|
||||||
|
|
@ -205,7 +197,7 @@ export function parseCalendarDateEvent(event: NostrEvent): CalendarDateEvent | n
|
||||||
content: event.content,
|
content: event.content,
|
||||||
image: getTagValue(event.tags, 'image'),
|
image: getTagValue(event.tags, 'image'),
|
||||||
start,
|
start,
|
||||||
end,
|
end: getTagValue(event.tags, 'end'),
|
||||||
location: getTagValue(event.tags, 'location'),
|
location: getTagValue(event.tags, 'location'),
|
||||||
geohash: getTagValue(event.tags, 'g'),
|
geohash: getTagValue(event.tags, 'g'),
|
||||||
hashtags: getTagValues(event.tags, 't'),
|
hashtags: getTagValues(event.tags, 't'),
|
||||||
|
|
|
||||||
|
|
@ -44,10 +44,6 @@ export interface TicketPaymentStatus {
|
||||||
/**
|
/**
|
||||||
* LNbits events extension event (database-backed ticketed event).
|
* LNbits events extension event (database-backed ticketed event).
|
||||||
* Corresponds to the Event model in the events extension.
|
* 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 {
|
export interface TicketedEvent {
|
||||||
id: string
|
id: string
|
||||||
|
|
|
||||||
|
|
@ -33,10 +33,7 @@ function formatDate(dateStr: string | null | undefined) {
|
||||||
if (!dateStr) return 'Date not available'
|
if (!dateStr) return 'Date not available'
|
||||||
const date = new Date(dateStr)
|
const date = new Date(dateStr)
|
||||||
if (isNaN(date.getTime())) return 'Invalid date'
|
if (isNaN(date.getTime())) return 'Invalid date'
|
||||||
// Presence of "T" in the wire value marks a time-based event (NIP-52
|
return format(date, 'MMMM do, yyyy')
|
||||||
// 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')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handlePurchaseClick(event: {
|
function handlePurchaseClick(event: {
|
||||||
|
|
|
||||||
|
|
@ -1,98 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed, ref } from 'vue'
|
|
||||||
import { CalendarDate, parseDate, type DateValue } from '@internationalized/date'
|
|
||||||
import { format } from 'date-fns'
|
|
||||||
import { Calendar as CalendarIcon, X } from 'lucide-vue-next'
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { Calendar } from '@/components/ui/calendar'
|
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
modelValue: string
|
|
||||||
placeholder?: string
|
|
||||||
/** Minimum selectable date as YYYY-MM-DD. */
|
|
||||||
min?: string
|
|
||||||
disabled?: boolean
|
|
||||||
clearable?: boolean
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
'update:modelValue': [value: string]
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const open = ref(false)
|
|
||||||
|
|
||||||
// Bridge between the wire format (YYYY-MM-DD string) and reka-ui's
|
|
||||||
// CalendarDate. Empty string ↔ undefined keeps the field optional.
|
|
||||||
// Closes the popover on user pick to match the shadcn-vue docs example
|
|
||||||
// — the Calendar component doesn't auto-close by itself.
|
|
||||||
const dateValue = computed<DateValue | undefined>({
|
|
||||||
get() {
|
|
||||||
if (!props.modelValue) return undefined
|
|
||||||
try {
|
|
||||||
return parseDate(props.modelValue)
|
|
||||||
} catch {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
},
|
|
||||||
set(v) {
|
|
||||||
if (!v) {
|
|
||||||
emit('update:modelValue', '')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const d = new CalendarDate(v.year, v.month, v.day)
|
|
||||||
emit('update:modelValue', d.toString())
|
|
||||||
open.value = false
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const minDate = computed<DateValue | undefined>(() => {
|
|
||||||
if (!props.min) return undefined
|
|
||||||
try {
|
|
||||||
return parseDate(props.min)
|
|
||||||
} catch {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const display = computed(() => {
|
|
||||||
if (!props.modelValue) return ''
|
|
||||||
const d = new Date(props.modelValue + 'T00:00:00')
|
|
||||||
if (isNaN(d.getTime())) return ''
|
|
||||||
return format(d, 'PPP')
|
|
||||||
})
|
|
||||||
|
|
||||||
function clear(e: Event) {
|
|
||||||
e.stopPropagation()
|
|
||||||
emit('update:modelValue', '')
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Popover v-model:open="open">
|
|
||||||
<PopoverTrigger as-child>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
:disabled="disabled"
|
|
||||||
:class="cn('w-full justify-start text-left font-normal', !modelValue && 'text-muted-foreground')"
|
|
||||||
>
|
|
||||||
<CalendarIcon class="mr-2 h-4 w-4 shrink-0" />
|
|
||||||
<span class="truncate">{{ display || placeholder || 'Pick a date' }}</span>
|
|
||||||
<button
|
|
||||||
v-if="clearable && modelValue && !disabled"
|
|
||||||
type="button"
|
|
||||||
class="ml-auto -mr-1 inline-flex h-5 w-5 items-center justify-center rounded-sm opacity-60 hover:opacity-100"
|
|
||||||
@click="clear"
|
|
||||||
aria-label="Clear date"
|
|
||||||
>
|
|
||||||
<X class="h-3.5 w-3.5" />
|
|
||||||
</button>
|
|
||||||
</Button>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent class="w-auto p-0" align="start">
|
|
||||||
<Calendar v-model="dateValue" :min-value="minDate" initial-focus />
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
</template>
|
|
||||||
|
|
@ -160,7 +160,7 @@ import { Camera, X, Loader2, AlertCircle, Image } from 'lucide-vue-next'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||||
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
|
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 {
|
interface ImageWithMetadata extends UploadedImage {
|
||||||
isPrimary: boolean
|
isPrimary: boolean
|
||||||
|
|
@ -175,12 +175,6 @@ const props = defineProps<{
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
allowCamera?: boolean
|
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<{
|
const emit = defineEmits<{
|
||||||
|
|
@ -385,8 +379,7 @@ const uploadFiles = async (files: File[]) => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const uploadOptions = {
|
const uploadOptions = {
|
||||||
maxSizeMB: maxSizeMB.value,
|
maxSizeMB: maxSizeMB.value
|
||||||
compress: props.compress,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upload files with better error handling
|
// Upload files with better error handling
|
||||||
|
|
|
||||||
|
|
@ -1,106 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed } from 'vue'
|
|
||||||
import { Clock, X } from 'lucide-vue-next'
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from '@/components/ui/select'
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* HH:MM time picker — two shadcn <Select> dropdowns. Mobile-first:
|
|
||||||
* tap to pick from the native sheet / wheel. Clear button when set.
|
|
||||||
*/
|
|
||||||
const props = defineProps<{
|
|
||||||
modelValue: string
|
|
||||||
/** Minute granularity for the dropdown list. Default 15. */
|
|
||||||
minuteStep?: number
|
|
||||||
disabled?: boolean
|
|
||||||
clearable?: boolean
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
'update:modelValue': [value: string]
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const step = computed(() => props.minuteStep ?? 15)
|
|
||||||
|
|
||||||
const hours = Array.from({ length: 24 }, (_, i) => i.toString().padStart(2, '0'))
|
|
||||||
const minutes = computed(() =>
|
|
||||||
Array.from({ length: Math.floor(60 / step.value) }, (_, i) =>
|
|
||||||
(i * step.value).toString().padStart(2, '0')
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
const hour = computed<string>({
|
|
||||||
get() {
|
|
||||||
return props.modelValue.split(':')[0] ?? ''
|
|
||||||
},
|
|
||||||
set(h) {
|
|
||||||
const m = props.modelValue.split(':')[1] || '00'
|
|
||||||
emit('update:modelValue', `${h}:${m}`)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const minute = computed<string>({
|
|
||||||
get() {
|
|
||||||
return props.modelValue.split(':')[1] ?? ''
|
|
||||||
},
|
|
||||||
set(m) {
|
|
||||||
const h = props.modelValue.split(':')[0] || '00'
|
|
||||||
emit('update:modelValue', `${h}:${m}`)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const hasValue = computed(() => Boolean(props.modelValue))
|
|
||||||
|
|
||||||
function clear() {
|
|
||||||
emit('update:modelValue', '')
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div :class="cn('flex items-center gap-1.5', disabled && 'opacity-50')">
|
|
||||||
<Clock class="h-4 w-4 text-muted-foreground shrink-0" />
|
|
||||||
|
|
||||||
<Select v-model="hour" :disabled="disabled">
|
|
||||||
<SelectTrigger class="flex-1 min-w-0 px-2 tabular-nums" aria-label="Hours">
|
|
||||||
<SelectValue placeholder="HH" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent class="max-h-56">
|
|
||||||
<SelectItem v-for="h in hours" :key="h" :value="h" class="tabular-nums">
|
|
||||||
{{ h }}
|
|
||||||
</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
|
|
||||||
<span class="text-muted-foreground select-none">:</span>
|
|
||||||
|
|
||||||
<Select v-model="minute" :disabled="disabled">
|
|
||||||
<SelectTrigger class="flex-1 min-w-0 px-2 tabular-nums" aria-label="Minutes">
|
|
||||||
<SelectValue placeholder="MM" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent class="max-h-56">
|
|
||||||
<SelectItem v-for="m in minutes" :key="m" :value="m" class="tabular-nums">
|
|
||||||
{{ m }}
|
|
||||||
</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
v-if="clearable && hasValue && !disabled"
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
class="h-8 w-8 shrink-0"
|
|
||||||
@click="clear"
|
|
||||||
aria-label="Clear time"
|
|
||||||
>
|
|
||||||
<X class="h-3.5 w-3.5" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import imageCompression from 'browser-image-compression'
|
|
||||||
import { BaseService } from '@/core/base/BaseService'
|
import { BaseService } from '@/core/base/BaseService'
|
||||||
import type { ServiceMetadata } from '@/core/base/BaseService'
|
import type { ServiceMetadata } from '@/core/base/BaseService'
|
||||||
import appConfig from '@/app.config'
|
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 {
|
export interface ImageUploadOptions {
|
||||||
maxSizeMB?: number
|
maxSizeMB?: number
|
||||||
acceptedTypes?: string[]
|
acceptedTypes?: string[]
|
||||||
generateThumbnail?: boolean
|
generateThumbnail?: boolean
|
||||||
/** Enable client-side resize + re-encode before upload. */
|
|
||||||
compress?: boolean | CompressOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
const DEFAULT_COMPRESS: Required<CompressOptions> = {
|
|
||||||
maxSizeMB: 1,
|
|
||||||
maxWidthOrHeight: 1920,
|
|
||||||
initialQuality: 0.85,
|
|
||||||
fileType: 'image/webp',
|
|
||||||
useWebWorker: true,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ImageUrlOptions {
|
export interface ImageUrlOptions {
|
||||||
|
|
@ -103,12 +76,8 @@ export class ImageUploadService extends BaseService {
|
||||||
// Validate file
|
// Validate file
|
||||||
this.validateFile(file, options)
|
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()
|
const formData = new FormData()
|
||||||
formData.append('images[]', fileToUpload)
|
formData.append('images[]', file)
|
||||||
|
|
||||||
const response = await fetch(`${this.baseUrl}/image`, {
|
const response = await fetch(`${this.baseUrl}/image`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|
@ -322,50 +291,6 @@ export class ImageUploadService extends BaseService {
|
||||||
return alias
|
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<File> {
|
|
||||||
if (!options.compress) return file
|
|
||||||
|
|
||||||
const opts: Required<CompressOptions> = {
|
|
||||||
...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
|
* Validate file before upload
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue