Add printable guest sign studio and refine event imagery
@@ -7,6 +7,19 @@ Guests open a shareable event link, optionally add a name, email, and note,
|
|||||||
and upload photos. Event people approve what appears in the gallery.
|
and upload photos. Event people approve what appears in the gallery.
|
||||||
Originals stay full quality in S3-compatible object storage.
|
Originals stay full quality in S3-compatible object storage.
|
||||||
|
|
||||||
|
## Image processing
|
||||||
|
|
||||||
|
Uploads go directly to object storage; the background worker creates WebP
|
||||||
|
derivatives with Sharp. Displays fit within 2048 × 2048 (quality 82), thumbnails
|
||||||
|
within 400 × 400 (quality 75), and cropped 8:3 banners within 2400 × 900
|
||||||
|
(quality 82). Encoding uses effort 4. Small images are not enlarged. Display
|
||||||
|
copies are auto-oriented and omit source metadata; original uploads and exports
|
||||||
|
retain their original bytes.
|
||||||
|
|
||||||
|
Existing JPEG derivatives remain supported through their saved database keys.
|
||||||
|
Only newly processed images use `.webp` keys and `image/webp`; this change does
|
||||||
|
not bulk-regenerate existing images or delete their JPEGs.
|
||||||
|
|
||||||
## Repository
|
## Repository
|
||||||
|
|
||||||
```text
|
```text
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { NextConfig } from "next";
|
|||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
output: "standalone",
|
output: "standalone",
|
||||||
poweredByHeader: false,
|
poweredByHeader: false,
|
||||||
|
images: { qualities: [75, 85] },
|
||||||
async redirects() {
|
async redirects() {
|
||||||
return [{
|
return [{
|
||||||
source: "/:path*",
|
source: "/:path*",
|
||||||
|
|||||||
@@ -15,6 +15,9 @@
|
|||||||
"@album/database": "workspace:*",
|
"@album/database": "workspace:*",
|
||||||
"@album/email": "workspace:*",
|
"@album/email": "workspace:*",
|
||||||
"@album/storage": "workspace:*",
|
"@album/storage": "workspace:*",
|
||||||
|
"@fontsource-variable/funnel-display": "^5.3.0",
|
||||||
|
"@fontsource-variable/geologica": "^5.3.0",
|
||||||
|
"@fontsource-variable/inter": "^5.3.0",
|
||||||
"@radix-ui/react-popover": "^1.1.23",
|
"@radix-ui/react-popover": "^1.1.23",
|
||||||
"@tanstack/react-query": "^5.90.2",
|
"@tanstack/react-query": "^5.90.2",
|
||||||
"@trpc/client": "^11.4.3",
|
"@trpc/client": "^11.4.3",
|
||||||
@@ -27,13 +30,16 @@
|
|||||||
"date-fns": "^4.4.0",
|
"date-fns": "^4.4.0",
|
||||||
"dotenv": "^16.5.0",
|
"dotenv": "^16.5.0",
|
||||||
"drizzle-orm": "^0.45.2",
|
"drizzle-orm": "^0.45.2",
|
||||||
|
"heic-to": "^1.5.2",
|
||||||
"lucide-react": "^0.468.0",
|
"lucide-react": "^0.468.0",
|
||||||
"next": "16.2.12",
|
"next": "16.2.12",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
|
"qrcode": "^1.5.4",
|
||||||
"radix-ui": "^1.6.7",
|
"radix-ui": "^1.6.7",
|
||||||
"react": "19.2.8",
|
"react": "19.2.8",
|
||||||
"react-day-picker": "^9",
|
"react-day-picker": "^9",
|
||||||
"react-dom": "19.2.8",
|
"react-dom": "19.2.8",
|
||||||
|
"react-image-crop": "^11.1.2",
|
||||||
"server-only": "^0.0.1",
|
"server-only": "^0.0.1",
|
||||||
"shadcn": "^4.21.0",
|
"shadcn": "^4.21.0",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
@@ -46,9 +52,12 @@
|
|||||||
"@tailwindcss/postcss": "^4.1.10",
|
"@tailwindcss/postcss": "^4.1.10",
|
||||||
"@types/bun": "^1.3.14",
|
"@types/bun": "^1.3.14",
|
||||||
"@types/node": "^22.15.32",
|
"@types/node": "^22.15.32",
|
||||||
|
"@types/qrcode": "^1.5.6",
|
||||||
"@types/react": "19.2.17",
|
"@types/react": "19.2.17",
|
||||||
"@types/react-dom": "19.2.3",
|
"@types/react-dom": "19.2.3",
|
||||||
|
"jsqr": "^1.4.0",
|
||||||
"postcss": "^8.5.22",
|
"postcss": "^8.5.22",
|
||||||
|
"sharp": "0.35.3",
|
||||||
"tailwindcss": "^4.1.10",
|
"tailwindcss": "^4.1.10",
|
||||||
"typescript": "^5.8.3"
|
"typescript": "^5.8.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 1487 344" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||||
|
<g transform="matrix(1,0,0,1,-120.524,-1098.56)">
|
||||||
|
<g id="slant-round" serif:id="slant/round" transform="matrix(0.661785,0,0,0.661785,24.3307,784.241)">
|
||||||
|
<g transform="matrix(1.20551,0,0,1.20551,-840.208,509.804)">
|
||||||
|
<g transform="matrix(233.753,0,0,233.753,1308.09,362.817)">
|
||||||
|
<path d="M0.213,-0C0.171,-0 0.142,-0.007 0.125,-0.022C0.108,-0.036 0.099,-0.057 0.099,-0.085C0.099,-0.101 0.101,-0.119 0.105,-0.14C0.109,-0.161 0.113,-0.181 0.117,-0.201L0.215,-0.657L0.321,-0.657L0.222,-0.192C0.219,-0.178 0.216,-0.165 0.214,-0.153C0.212,-0.14 0.21,-0.129 0.21,-0.119C0.21,-0.108 0.214,-0.1 0.221,-0.095C0.228,-0.089 0.239,-0.087 0.253,-0.087L0.34,-0.087L0.322,-0L0.213,-0ZM0.089,-0.445L0.107,-0.532L0.435,-0.532L0.416,-0.445L0.089,-0.445Z" style="fill-rule:nonzero;stroke:black;stroke-width:0.01px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(233.753,0,0,233.753,1399.35,362.817)">
|
||||||
|
<path d="M0.232,0.012C0.169,0.012 0.12,-0.005 0.086,-0.04C0.052,-0.075 0.035,-0.123 0.035,-0.184C0.035,-0.234 0.042,-0.281 0.056,-0.325C0.07,-0.369 0.091,-0.407 0.117,-0.44C0.144,-0.473 0.177,-0.499 0.215,-0.517C0.254,-0.535 0.298,-0.544 0.346,-0.544C0.386,-0.544 0.42,-0.537 0.448,-0.523C0.476,-0.509 0.497,-0.487 0.512,-0.459C0.526,-0.431 0.533,-0.396 0.533,-0.354C0.533,-0.334 0.532,-0.313 0.529,-0.293C0.526,-0.272 0.522,-0.253 0.517,-0.235L0.129,-0.235L0.146,-0.315L0.422,-0.315C0.423,-0.321 0.424,-0.328 0.425,-0.335C0.425,-0.342 0.426,-0.351 0.426,-0.362C0.426,-0.392 0.417,-0.415 0.401,-0.431C0.384,-0.446 0.36,-0.454 0.33,-0.454C0.303,-0.454 0.277,-0.447 0.254,-0.434C0.231,-0.421 0.212,-0.403 0.196,-0.38C0.18,-0.356 0.167,-0.329 0.158,-0.297C0.149,-0.266 0.145,-0.231 0.145,-0.194C0.145,-0.154 0.154,-0.124 0.172,-0.106C0.19,-0.088 0.216,-0.079 0.251,-0.079C0.283,-0.079 0.31,-0.086 0.334,-0.1C0.357,-0.114 0.373,-0.134 0.383,-0.16L0.492,-0.153C0.47,-0.103 0.439,-0.063 0.397,-0.033C0.355,-0.003 0.3,0.012 0.232,0.012Z" style="fill-rule:nonzero;stroke:black;stroke-width:0.01px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(233.753,0,0,233.753,1529.27,362.817)">
|
||||||
|
<path d="M0.222,0.012C0.159,0.012 0.112,-0.005 0.081,-0.038C0.05,-0.071 0.035,-0.117 0.035,-0.177C0.035,-0.252 0.047,-0.316 0.073,-0.371C0.098,-0.426 0.134,-0.468 0.18,-0.499C0.225,-0.529 0.279,-0.544 0.34,-0.544C0.381,-0.544 0.416,-0.536 0.443,-0.521C0.471,-0.505 0.492,-0.483 0.507,-0.454C0.521,-0.425 0.528,-0.391 0.528,-0.351L0.418,-0.345C0.418,-0.383 0.41,-0.411 0.394,-0.427C0.379,-0.444 0.354,-0.452 0.321,-0.452C0.287,-0.452 0.256,-0.441 0.23,-0.418C0.203,-0.395 0.183,-0.364 0.168,-0.326C0.153,-0.288 0.145,-0.247 0.145,-0.201C0.145,-0.156 0.153,-0.125 0.168,-0.107C0.183,-0.089 0.207,-0.08 0.242,-0.08C0.278,-0.08 0.309,-0.089 0.334,-0.109C0.359,-0.128 0.379,-0.157 0.393,-0.195L0.501,-0.189C0.487,-0.148 0.467,-0.113 0.441,-0.083C0.415,-0.053 0.383,-0.029 0.347,-0.013C0.31,0.004 0.269,0.012 0.222,0.012Z" style="fill-rule:nonzero;stroke:black;stroke-width:0.01px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(233.753,0,0,233.753,1656.24,362.817)">
|
||||||
|
<path d="M0.02,-0L0.17,-0.71L0.276,-0.71L0.213,-0.412L0.198,-0.415C0.218,-0.458 0.245,-0.491 0.278,-0.512C0.311,-0.533 0.347,-0.544 0.386,-0.544C0.434,-0.544 0.469,-0.531 0.493,-0.506C0.517,-0.481 0.529,-0.448 0.529,-0.407C0.529,-0.39 0.527,-0.371 0.524,-0.351C0.52,-0.331 0.517,-0.313 0.513,-0.299L0.45,-0L0.344,-0L0.41,-0.309C0.413,-0.321 0.415,-0.333 0.417,-0.345C0.419,-0.358 0.42,-0.371 0.42,-0.384C0.42,-0.406 0.413,-0.424 0.4,-0.436C0.387,-0.449 0.368,-0.455 0.341,-0.455C0.318,-0.455 0.296,-0.45 0.276,-0.439C0.256,-0.428 0.239,-0.412 0.224,-0.391C0.21,-0.37 0.199,-0.344 0.192,-0.312L0.126,-0L0.02,-0Z" style="fill-rule:nonzero;stroke:black;stroke-width:0.01px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(233.753,0,0,233.753,1787.71,362.817)">
|
||||||
|
<path d="M0.019,-0L0.132,-0.532L0.229,-0.532L0.202,-0.39L0.191,-0.397C0.205,-0.431 0.222,-0.459 0.242,-0.481C0.262,-0.502 0.285,-0.518 0.309,-0.529C0.334,-0.539 0.359,-0.544 0.386,-0.544C0.434,-0.544 0.469,-0.531 0.493,-0.506C0.517,-0.481 0.528,-0.449 0.528,-0.409C0.528,-0.391 0.527,-0.372 0.523,-0.351C0.52,-0.331 0.517,-0.313 0.513,-0.299L0.45,-0L0.344,-0L0.41,-0.309C0.413,-0.321 0.415,-0.333 0.417,-0.346C0.419,-0.359 0.42,-0.373 0.42,-0.387C0.42,-0.409 0.413,-0.425 0.4,-0.437C0.387,-0.449 0.367,-0.455 0.341,-0.455C0.318,-0.455 0.296,-0.449 0.276,-0.438C0.256,-0.427 0.238,-0.41 0.224,-0.389C0.209,-0.367 0.198,-0.341 0.191,-0.31L0.125,-0L0.019,-0Z" style="fill-rule:nonzero;stroke:black;stroke-width:0.01px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(233.753,0,0,233.753,1920.76,362.817)">
|
||||||
|
<path d="M0.23,0.012C0.166,0.012 0.118,-0.005 0.084,-0.04C0.051,-0.075 0.034,-0.121 0.034,-0.177C0.034,-0.225 0.04,-0.271 0.053,-0.315C0.065,-0.359 0.084,-0.398 0.109,-0.432C0.134,-0.467 0.165,-0.494 0.203,-0.514C0.24,-0.534 0.283,-0.544 0.333,-0.544C0.396,-0.544 0.445,-0.527 0.478,-0.492C0.511,-0.458 0.527,-0.412 0.527,-0.355C0.527,-0.307 0.521,-0.261 0.509,-0.217C0.496,-0.173 0.478,-0.134 0.453,-0.1C0.428,-0.065 0.397,-0.038 0.36,-0.018C0.323,0.002 0.279,0.012 0.23,0.012ZM0.244,-0.08C0.279,-0.08 0.309,-0.091 0.335,-0.114C0.361,-0.137 0.381,-0.169 0.395,-0.208C0.41,-0.247 0.417,-0.291 0.417,-0.339C0.417,-0.378 0.409,-0.407 0.393,-0.425C0.377,-0.443 0.352,-0.452 0.318,-0.452C0.284,-0.452 0.253,-0.441 0.227,-0.418C0.201,-0.394 0.181,-0.363 0.166,-0.324C0.152,-0.285 0.144,-0.241 0.144,-0.193C0.144,-0.153 0.153,-0.124 0.17,-0.107C0.186,-0.089 0.211,-0.08 0.244,-0.08Z" style="fill-rule:nonzero;stroke:black;stroke-width:0.01px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(233.753,0,0,233.753,2051.05,362.817)">
|
||||||
|
<path d="M0.132,-0C0.102,-0 0.079,-0.006 0.063,-0.017C0.047,-0.029 0.039,-0.047 0.039,-0.072C0.039,-0.088 0.041,-0.105 0.046,-0.123C0.05,-0.142 0.053,-0.159 0.057,-0.175L0.17,-0.71L0.276,-0.71L0.161,-0.167C0.158,-0.154 0.156,-0.143 0.154,-0.132C0.151,-0.121 0.15,-0.113 0.15,-0.106C0.15,-0.1 0.153,-0.095 0.157,-0.092C0.162,-0.088 0.169,-0.087 0.178,-0.087L0.223,-0.087L0.205,-0L0.132,-0Z" style="fill-rule:nonzero;stroke:black;stroke-width:0.01px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(233.753,0,0,233.753,2113.05,362.817)">
|
||||||
|
<path d="M0.23,0.012C0.166,0.012 0.118,-0.005 0.084,-0.04C0.051,-0.075 0.034,-0.121 0.034,-0.177C0.034,-0.225 0.04,-0.271 0.053,-0.315C0.065,-0.359 0.084,-0.398 0.109,-0.432C0.134,-0.467 0.165,-0.494 0.203,-0.514C0.24,-0.534 0.283,-0.544 0.333,-0.544C0.396,-0.544 0.445,-0.527 0.478,-0.492C0.511,-0.458 0.527,-0.412 0.527,-0.355C0.527,-0.307 0.521,-0.261 0.509,-0.217C0.496,-0.173 0.478,-0.134 0.453,-0.1C0.428,-0.065 0.397,-0.038 0.36,-0.018C0.323,0.002 0.279,0.012 0.23,0.012ZM0.244,-0.08C0.279,-0.08 0.309,-0.091 0.335,-0.114C0.361,-0.137 0.381,-0.169 0.395,-0.208C0.41,-0.247 0.417,-0.291 0.417,-0.339C0.417,-0.378 0.409,-0.407 0.393,-0.425C0.377,-0.443 0.352,-0.452 0.318,-0.452C0.284,-0.452 0.253,-0.441 0.227,-0.418C0.201,-0.394 0.181,-0.363 0.166,-0.324C0.152,-0.285 0.144,-0.241 0.144,-0.193C0.144,-0.153 0.153,-0.124 0.17,-0.107C0.186,-0.089 0.211,-0.08 0.244,-0.08Z" style="fill-rule:nonzero;stroke:black;stroke-width:0.01px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(233.753,0,0,233.753,2242.17,362.817)">
|
||||||
|
<path d="M0.192,0.162C0.158,0.162 0.128,0.159 0.104,0.152C0.08,0.145 0.06,0.134 0.045,0.121C0.029,0.108 0.018,0.092 0.011,0.073C0.004,0.054 0.001,0.033 0.001,0.009L0.111,0.002C0.111,0.025 0.118,0.043 0.132,0.056C0.145,0.068 0.171,0.074 0.209,0.074C0.255,0.074 0.289,0.065 0.313,0.048C0.337,0.032 0.353,0.003 0.361,-0.038L0.378,-0.116C0.359,-0.089 0.336,-0.068 0.308,-0.052C0.28,-0.035 0.247,-0.027 0.21,-0.027C0.156,-0.027 0.114,-0.044 0.085,-0.077C0.055,-0.11 0.041,-0.156 0.041,-0.216C0.041,-0.26 0.047,-0.302 0.06,-0.341C0.073,-0.381 0.091,-0.415 0.115,-0.446C0.139,-0.476 0.167,-0.5 0.201,-0.518C0.234,-0.535 0.272,-0.544 0.314,-0.544C0.355,-0.544 0.386,-0.535 0.408,-0.518C0.431,-0.5 0.445,-0.477 0.451,-0.448L0.469,-0.532L0.571,-0.532L0.467,-0.04C0.457,0.011 0.44,0.051 0.416,0.08C0.393,0.11 0.363,0.131 0.326,0.143C0.289,0.156 0.244,0.162 0.192,0.162ZM0.243,-0.117C0.272,-0.117 0.297,-0.124 0.319,-0.138C0.342,-0.151 0.36,-0.169 0.375,-0.192C0.39,-0.214 0.402,-0.238 0.409,-0.265C0.417,-0.291 0.42,-0.318 0.42,-0.344C0.42,-0.378 0.411,-0.404 0.393,-0.424C0.375,-0.444 0.35,-0.454 0.317,-0.454C0.292,-0.454 0.27,-0.448 0.25,-0.437C0.23,-0.425 0.212,-0.409 0.198,-0.389C0.183,-0.368 0.171,-0.345 0.164,-0.317C0.156,-0.29 0.152,-0.261 0.152,-0.229C0.152,-0.191 0.158,-0.164 0.172,-0.145C0.185,-0.126 0.209,-0.117 0.243,-0.117Z" style="fill-rule:nonzero;stroke:black;stroke-width:0.01px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(233.753,0,0,233.753,2377.23,362.817)">
|
||||||
|
<path d="M0.019,-0L0.131,-0.532L0.237,-0.532L0.125,-0L0.019,-0ZM0.146,-0.609L0.167,-0.715L0.278,-0.715L0.256,-0.609L0.146,-0.609Z" style="fill-rule:nonzero;stroke:black;stroke-width:0.01px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(233.753,0,0,233.753,2435.77,362.817)">
|
||||||
|
<path d="M0.232,0.012C0.169,0.012 0.12,-0.005 0.086,-0.04C0.052,-0.075 0.035,-0.123 0.035,-0.184C0.035,-0.234 0.042,-0.281 0.056,-0.325C0.07,-0.369 0.091,-0.407 0.117,-0.44C0.144,-0.473 0.177,-0.499 0.215,-0.517C0.254,-0.535 0.298,-0.544 0.346,-0.544C0.386,-0.544 0.42,-0.537 0.448,-0.523C0.476,-0.509 0.497,-0.487 0.512,-0.459C0.526,-0.431 0.533,-0.396 0.533,-0.354C0.533,-0.334 0.532,-0.313 0.529,-0.293C0.526,-0.272 0.522,-0.253 0.517,-0.235L0.129,-0.235L0.146,-0.315L0.422,-0.315C0.423,-0.321 0.424,-0.328 0.425,-0.335C0.425,-0.342 0.426,-0.351 0.426,-0.362C0.426,-0.392 0.417,-0.415 0.401,-0.431C0.384,-0.446 0.36,-0.454 0.33,-0.454C0.303,-0.454 0.277,-0.447 0.254,-0.434C0.231,-0.421 0.212,-0.403 0.196,-0.38C0.18,-0.356 0.167,-0.329 0.158,-0.297C0.149,-0.266 0.145,-0.231 0.145,-0.194C0.145,-0.154 0.154,-0.124 0.172,-0.106C0.19,-0.088 0.216,-0.079 0.251,-0.079C0.283,-0.079 0.31,-0.086 0.334,-0.1C0.357,-0.114 0.373,-0.134 0.383,-0.16L0.492,-0.153C0.47,-0.103 0.439,-0.063 0.397,-0.033C0.355,-0.003 0.3,0.012 0.232,0.012Z" style="fill-rule:nonzero;stroke:black;stroke-width:0.01px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(233.753,0,0,233.753,2565.69,362.817)">
|
||||||
|
<path d="M0.236,0.012C0.172,0.012 0.122,-0.004 0.085,-0.036C0.048,-0.068 0.027,-0.112 0.022,-0.168L0.131,-0.173C0.135,-0.14 0.147,-0.115 0.166,-0.1C0.185,-0.084 0.212,-0.077 0.247,-0.077C0.277,-0.077 0.302,-0.082 0.324,-0.094C0.345,-0.105 0.356,-0.123 0.356,-0.148C0.356,-0.161 0.353,-0.171 0.348,-0.18C0.342,-0.188 0.332,-0.196 0.316,-0.202C0.3,-0.207 0.277,-0.213 0.245,-0.218C0.188,-0.228 0.144,-0.245 0.115,-0.268C0.085,-0.292 0.07,-0.326 0.07,-0.371C0.07,-0.4 0.078,-0.427 0.092,-0.454C0.106,-0.48 0.129,-0.501 0.16,-0.519C0.192,-0.536 0.234,-0.544 0.286,-0.544C0.349,-0.544 0.398,-0.529 0.433,-0.498C0.468,-0.468 0.488,-0.425 0.493,-0.369L0.383,-0.363C0.381,-0.395 0.371,-0.419 0.354,-0.433C0.337,-0.448 0.312,-0.455 0.281,-0.455C0.246,-0.455 0.22,-0.447 0.204,-0.432C0.188,-0.416 0.181,-0.397 0.181,-0.375C0.181,-0.361 0.184,-0.35 0.191,-0.341C0.198,-0.331 0.209,-0.324 0.224,-0.318C0.24,-0.312 0.26,-0.307 0.286,-0.302C0.332,-0.294 0.369,-0.283 0.395,-0.271C0.421,-0.258 0.44,-0.242 0.45,-0.224C0.461,-0.205 0.466,-0.182 0.466,-0.156C0.466,-0.12 0.456,-0.09 0.436,-0.065C0.416,-0.039 0.388,-0.02 0.354,-0.007C0.319,0.006 0.28,0.012 0.236,0.012Z" style="fill-rule:nonzero;stroke:black;stroke-width:0.01px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(233.753,0,0,233.753,2690.79,362.817)">
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.20551,0,0,1.20551,-797.429,238.41)">
|
||||||
|
<g transform="matrix(233.753,0,0,233.753,1308.09,362.817)">
|
||||||
|
<path d="M0.01,-0L0.16,-0.71L0.31,-0.71L0.246,-0.411L0.227,-0.412C0.247,-0.459 0.274,-0.493 0.307,-0.515C0.34,-0.537 0.377,-0.548 0.418,-0.548C0.465,-0.548 0.501,-0.536 0.525,-0.511C0.55,-0.486 0.562,-0.453 0.562,-0.41C0.562,-0.39 0.56,-0.368 0.556,-0.345C0.552,-0.322 0.548,-0.301 0.544,-0.284L0.483,-0L0.333,-0L0.397,-0.294C0.4,-0.306 0.402,-0.319 0.404,-0.332C0.406,-0.345 0.407,-0.358 0.407,-0.371C0.407,-0.39 0.402,-0.404 0.392,-0.414C0.382,-0.424 0.366,-0.429 0.344,-0.429C0.325,-0.429 0.307,-0.425 0.291,-0.416C0.275,-0.407 0.262,-0.394 0.251,-0.375C0.239,-0.357 0.231,-0.334 0.225,-0.305L0.16,-0L0.01,-0Z" style="fill-rule:nonzero;stroke:black;stroke-width:0.01px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(233.753,0,0,233.753,1446.75,362.817)">
|
||||||
|
<path d="M0.381,-0.244L0.242,-0.214C0.217,-0.209 0.198,-0.201 0.186,-0.189C0.173,-0.178 0.167,-0.161 0.167,-0.139C0.167,-0.123 0.172,-0.111 0.18,-0.103C0.189,-0.095 0.202,-0.091 0.22,-0.091C0.242,-0.091 0.261,-0.095 0.279,-0.102C0.297,-0.11 0.312,-0.121 0.325,-0.138C0.337,-0.154 0.346,-0.174 0.352,-0.199C0.354,-0.21 0.356,-0.224 0.36,-0.24C0.363,-0.257 0.367,-0.274 0.37,-0.292C0.374,-0.311 0.377,-0.327 0.379,-0.343C0.381,-0.358 0.382,-0.371 0.382,-0.379C0.382,-0.399 0.378,-0.414 0.369,-0.424C0.359,-0.434 0.343,-0.439 0.318,-0.439C0.291,-0.439 0.27,-0.433 0.254,-0.421C0.238,-0.408 0.225,-0.389 0.216,-0.362L0.067,-0.369C0.087,-0.43 0.118,-0.475 0.16,-0.504C0.203,-0.533 0.263,-0.548 0.341,-0.548C0.406,-0.548 0.454,-0.535 0.486,-0.508C0.517,-0.482 0.533,-0.442 0.533,-0.391C0.533,-0.377 0.531,-0.361 0.528,-0.341C0.525,-0.32 0.521,-0.299 0.516,-0.277C0.511,-0.254 0.506,-0.233 0.501,-0.212C0.496,-0.191 0.492,-0.172 0.489,-0.156C0.486,-0.141 0.484,-0.13 0.484,-0.124C0.484,-0.116 0.486,-0.11 0.49,-0.107C0.495,-0.104 0.501,-0.103 0.509,-0.103L0.524,-0.103L0.503,-0C0.497,0.001 0.489,0.003 0.479,0.004C0.468,0.005 0.457,0.006 0.447,0.006C0.414,0.006 0.389,-0.001 0.373,-0.014C0.356,-0.028 0.348,-0.046 0.348,-0.069C0.348,-0.077 0.349,-0.084 0.35,-0.092C0.351,-0.1 0.353,-0.108 0.356,-0.116L0.365,-0.108C0.354,-0.084 0.339,-0.063 0.32,-0.045C0.301,-0.027 0.279,-0.013 0.253,-0.003C0.227,0.007 0.197,0.012 0.164,0.012C0.116,0.012 0.079,0.002 0.052,-0.019C0.025,-0.04 0.012,-0.07 0.012,-0.11C0.012,-0.163 0.029,-0.204 0.063,-0.235C0.097,-0.265 0.146,-0.286 0.211,-0.299L0.401,-0.337L0.381,-0.244Z" style="fill-rule:nonzero;stroke:black;stroke-width:0.01px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(233.753,0,0,233.753,1577.98,362.817)">
|
||||||
|
<path d="M0.19,0.012C0.136,0.012 0.096,-0.005 0.07,-0.04C0.043,-0.075 0.03,-0.122 0.03,-0.182C0.03,-0.233 0.037,-0.281 0.051,-0.325C0.065,-0.369 0.084,-0.408 0.11,-0.442C0.135,-0.475 0.165,-0.501 0.199,-0.52C0.233,-0.539 0.27,-0.548 0.311,-0.548C0.345,-0.548 0.373,-0.54 0.396,-0.524C0.418,-0.508 0.434,-0.487 0.442,-0.46L0.495,-0.71L0.645,-0.71L0.494,-0L0.351,-0L0.373,-0.122L0.383,-0.11C0.362,-0.072 0.336,-0.043 0.304,-0.021C0.273,0.001 0.235,0.012 0.19,0.012ZM0.259,-0.103C0.28,-0.103 0.3,-0.109 0.318,-0.121C0.336,-0.133 0.352,-0.149 0.365,-0.17C0.378,-0.191 0.389,-0.216 0.397,-0.244C0.405,-0.272 0.408,-0.303 0.408,-0.336C0.408,-0.367 0.401,-0.391 0.388,-0.408C0.374,-0.424 0.355,-0.433 0.331,-0.433C0.31,-0.433 0.291,-0.427 0.274,-0.415C0.256,-0.403 0.241,-0.386 0.227,-0.366C0.214,-0.345 0.203,-0.321 0.196,-0.294C0.188,-0.267 0.185,-0.238 0.185,-0.207C0.185,-0.173 0.191,-0.148 0.203,-0.13C0.214,-0.112 0.233,-0.103 0.259,-0.103Z" style="fill-rule:nonzero;stroke:black;stroke-width:0.01px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(233.753,0,0,233.753,1720.76,362.817)">
|
||||||
|
<path d="M0.143,-0C0.107,-0 0.079,-0.007 0.061,-0.021C0.043,-0.035 0.034,-0.057 0.034,-0.088C0.034,-0.105 0.036,-0.124 0.041,-0.146C0.045,-0.168 0.16,-0.71 0.16,-0.71L0.31,-0.71L0.202,-0.202C0.2,-0.189 0.197,-0.176 0.195,-0.164C0.192,-0.152 0.191,-0.142 0.191,-0.135C0.191,-0.127 0.194,-0.121 0.198,-0.118C0.203,-0.114 0.211,-0.112 0.222,-0.112L0.262,-0.112L0.239,-0L0.143,-0Z" style="fill-rule:nonzero;stroke:black;stroke-width:0.01px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(233.753,0,0,233.753,1789.86,362.817)">
|
||||||
|
<path d="M0.245,0.012C0.175,0.012 0.122,-0.005 0.085,-0.04C0.049,-0.075 0.031,-0.123 0.031,-0.184C0.031,-0.233 0.037,-0.279 0.05,-0.323C0.062,-0.367 0.082,-0.405 0.108,-0.439C0.135,-0.473 0.168,-0.5 0.208,-0.519C0.248,-0.538 0.296,-0.548 0.35,-0.548C0.421,-0.548 0.474,-0.531 0.51,-0.496C0.546,-0.461 0.564,-0.413 0.564,-0.352C0.564,-0.303 0.558,-0.257 0.545,-0.213C0.532,-0.169 0.513,-0.13 0.487,-0.097C0.46,-0.063 0.427,-0.036 0.387,-0.017C0.347,0.002 0.3,0.012 0.245,0.012ZM0.265,-0.103C0.294,-0.103 0.319,-0.114 0.34,-0.134C0.362,-0.155 0.379,-0.182 0.391,-0.217C0.403,-0.252 0.41,-0.29 0.41,-0.333C0.41,-0.367 0.403,-0.392 0.39,-0.408C0.376,-0.424 0.357,-0.433 0.33,-0.433C0.301,-0.433 0.276,-0.422 0.254,-0.402C0.232,-0.381 0.215,-0.353 0.203,-0.318C0.191,-0.284 0.185,-0.245 0.185,-0.202C0.185,-0.169 0.192,-0.144 0.206,-0.128C0.22,-0.111 0.239,-0.103 0.265,-0.103Z" style="fill-rule:nonzero;stroke:black;stroke-width:0.01px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(233.753,0,0,233.753,1926.65,362.817)">
|
||||||
|
<path d="M0.238,0.012C0.169,0.012 0.117,-0.005 0.082,-0.038C0.048,-0.071 0.03,-0.12 0.03,-0.184C0.03,-0.259 0.044,-0.324 0.071,-0.378C0.099,-0.432 0.137,-0.474 0.186,-0.504C0.234,-0.533 0.291,-0.548 0.355,-0.548C0.401,-0.548 0.44,-0.54 0.471,-0.524C0.502,-0.507 0.525,-0.483 0.541,-0.452C0.556,-0.421 0.564,-0.383 0.564,-0.338L0.409,-0.331C0.409,-0.367 0.403,-0.393 0.391,-0.409C0.379,-0.424 0.359,-0.432 0.33,-0.432C0.302,-0.432 0.277,-0.422 0.255,-0.402C0.234,-0.382 0.216,-0.355 0.204,-0.321C0.191,-0.287 0.185,-0.249 0.185,-0.206C0.185,-0.17 0.191,-0.143 0.204,-0.127C0.216,-0.111 0.236,-0.103 0.263,-0.103C0.293,-0.103 0.319,-0.112 0.34,-0.13C0.361,-0.147 0.378,-0.174 0.39,-0.21L0.543,-0.204C0.529,-0.16 0.508,-0.122 0.479,-0.089C0.45,-0.057 0.415,-0.032 0.374,-0.014C0.334,0.003 0.288,0.012 0.238,0.012Z" style="fill-rule:nonzero;stroke:black;stroke-width:0.01px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(233.753,0,0,233.753,2061.9,362.817)">
|
||||||
|
<path d="M0.16,-0.131L0.192,-0.306L0.45,-0.536L0.637,-0.536L0.16,-0.131ZM0.01,-0L0.16,-0.71L0.31,-0.71L0.16,-0L0.01,-0ZM0.368,-0L0.264,-0.248L0.379,-0.333L0.533,-0L0.368,-0Z" style="fill-rule:nonzero;stroke:black;stroke-width:0.01px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(233.753,0,0,233.753,2204.53,362.817)">
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(233.753,0,0,233.753,2257.92,362.817)">
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.96583,0,0,0.984036,-9.32219,21.63)">
|
||||||
|
<path d="M311.416,461.453L281.645,600.375C289.122,590.297 297.573,582.144 306.999,575.916C323.911,564.741 342.767,559.153 363.566,559.153C387.706,559.153 405.998,565.418 418.443,577.948C430.888,590.478 437.111,607.649 437.111,629.46C437.111,639.413 436.088,650.35 434.042,662.271C431.997,674.193 430.02,684.453 428.113,693.051L427.462,696.076C423.753,695.216 420.018,694.752 416.268,694.75C406.179,694.744 395.48,698.167 384.836,704.403C372.642,711.548 360.183,722.586 348.622,733.745C346.354,735.934 343.856,738.282 341.291,740.656L352.618,688.384C354.19,682.014 355.504,675.476 356.562,668.772C357.619,662.068 358.148,655.533 358.148,649.167C358.148,639.479 355.506,632.129 350.221,627.118C344.936,622.107 336.803,619.602 325.823,619.602C315.935,619.602 306.916,621.837 298.768,626.309C290.619,630.78 283.68,637.62 277.95,646.83C272.22,656.039 267.786,667.925 264.649,682.487L251.522,744.09C238.026,749.008 223.339,759.127 209.691,775.266C195.858,791.625 174.257,805.089 160.149,812.821L222.685,520.219C248.05,495.448 277.874,475.362 311.416,461.453ZM167.579,829.767C182.86,821.112 206.229,805.095 222.641,785.686C237.826,767.728 254.036,758.385 267.374,757.899C293.757,756.936 302.691,778.88 318.086,779.007C336.919,779.162 379.548,712.1 414.23,712.121C451.598,712.144 483.219,775.838 507.785,775.779C524.374,775.74 533.917,757.587 553.577,757.745C563.589,757.825 577.104,768.353 591.118,779.64C602.675,788.947 640.35,820.705 666.264,842.607C619.483,926.992 528.796,985.05 423.739,986.884C310.019,988.869 213.48,924.048 167.579,829.767ZM278.49,839.21C278.49,839.21 304.093,836.136 326.031,822.275C348.428,808.124 369.348,792.845 375.016,792.822C384.512,792.783 389.858,806.521 399.148,806.075C412.642,805.428 428.651,781.005 442.132,780.304C453.181,779.73 462.606,790.088 473.411,793.844C485.308,797.98 493.412,795.767 493.412,795.767C493.412,795.767 485.653,791.267 474.788,781.04C468.553,775.172 455.548,760.741 448.613,753.468C439.024,743.412 431.096,732.407 414.23,732.443C396.454,732.481 378.823,753.996 363.705,769.996C361.805,772.006 346.912,787.343 326.504,804.966C305.719,822.914 278.49,839.21 278.49,839.21ZM191.013,835.528C191.013,835.528 198.753,834.654 212.863,828.48C234.834,818.867 248.25,808.244 253.605,808.284C263.1,808.356 269.553,814.837 277.343,814.911C289.97,815.032 304.884,797.976 304.884,797.976C304.884,797.976 300.044,793.491 292.116,788.043C284.572,782.858 276.793,777.654 270.195,778.095C262.994,778.577 254.156,783.666 233.748,801.289C212.963,819.238 191.013,835.528 191.013,835.528ZM444.972,847.714C444.972,847.714 460.408,849.651 480.379,840.466C501.271,830.857 510.121,817.267 521.837,817.219C531.333,817.18 531.587,823.077 543.15,823.2C549.857,823.271 564.23,810.962 570.599,808.858C576.701,806.843 587.107,812.929 593.443,813.343C601.094,813.844 609.904,813.566 609.904,813.566C609.904,813.566 594.304,800.885 580.945,790.381C565.983,778.617 562.447,772.447 552.368,772.483C534.593,772.547 524.153,792.804 507.527,805.859C502.985,809.424 491.579,822.338 467.899,834.348C444.737,846.095 444.972,847.714 444.972,847.714ZM215.601,885.767C215.791,895.51 391.383,897.206 391.75,900.77C392.145,904.604 279.347,904.07 279.388,911.079C279.421,916.747 388.247,915.497 388.209,918.468C388.157,922.422 307.862,929.231 307.906,933.11C307.947,936.729 417.063,938.623 417.252,942.432C417.439,946.187 374.935,948.552 374.932,952.509C374.93,955.439 393.626,958.15 436.923,958.622C462.095,958.896 485.516,954.565 485.422,951.428C485.303,947.447 449.314,945.119 449.219,942.655C449.124,940.205 531.133,936.516 531.261,931.696C531.368,927.625 442.356,921.757 442.276,919.178C442.212,917.13 558.319,915.343 558.409,911.079C558.473,908.009 443,903.452 443.03,900.77C443.055,898.507 621.947,890.989 621.84,886.225C621.789,883.919 521.675,884.701 416.688,884.351C318.799,884.024 215.535,882.359 215.601,885.767ZM681.616,619.738L595.583,619.766L574.301,719.027C572.647,726.603 571.143,733.885 569.788,740.872C564.39,739.115 559.011,738.144 553.734,738.101C541.83,738.006 532.257,741.958 523.591,747.203C520.143,749.29 516.882,751.593 513.572,753.607C511.888,754.632 510.324,755.863 508.379,756.096C507.433,755.535 503.282,753.035 500.771,750.897C498.037,748.569 495.24,745.913 492.378,743.066C494.161,734.289 496.032,725.328 497.991,716.185L518.753,619.766L474.568,619.766L486.739,563.071L531.413,563.071L550.495,471.901C575.034,484.681 597.215,501.04 616.762,520.085L608.019,563.071L652.508,563.071C664.182,580.704 674.281,599.613 681.616,619.738Z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 23 KiB |
@@ -0,0 +1,93 @@
|
|||||||
|
Copyright 2023 The Funnel Project Authors (https://github.com/Dicotype/Funnel)
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
http://scripts.sil.org/OFL
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
Copyright 2020 The Geologica Project Authors (https://github.com/googlefonts/geologica)
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
http://scripts.sil.org/OFL
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
Copyright 2016 The Inter Project Authors (https://github.com/rsms/inter) Inter-Italic[opsz,wght].ttf: Copyright 2016 The Inter Project Authors (https://github.com/rsms/inter)
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
http://scripts.sil.org/OFL
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
@@ -246,22 +246,24 @@ export function EventSettingsForm({
|
|||||||
<Button type="button" variant="outline" disabled={bannerBusy || (!formBanner && !formCustomBanner)} onClick={() => { setFormBanner(null); setFormCustomBanner(null); }}>Remove banner</Button>
|
<Button type="button" variant="outline" disabled={bannerBusy || (!formBanner && !formCustomBanner)} onClick={() => { setFormBanner(null); setFormCustomBanner(null); }}>Remove banner</Button>
|
||||||
<details className="rounded-lg border p-3">
|
<details className="rounded-lg border p-3">
|
||||||
<summary className="cursor-pointer py-2 text-sm font-medium">Choose from approved gallery photos ({bannerPhotos.length})</summary>
|
<summary className="cursor-pointer py-2 text-sm font-medium">Choose from approved gallery photos ({bannerPhotos.length})</summary>
|
||||||
<fieldset disabled={bannerBusy} aria-labelledby="banner-label" className="mt-3 grid max-h-80 grid-cols-2 gap-3 overflow-y-auto sm:grid-cols-3">
|
<div className="mt-3 max-h-80 overflow-y-auto p-1">
|
||||||
|
<fieldset disabled={bannerBusy} aria-labelledby="banner-label" className="grid min-w-0 auto-rows-max grid-cols-2 gap-3 sm:grid-cols-3">
|
||||||
{bannerPhotos.map((photo, index) => (
|
{bannerPhotos.map((photo, index) => (
|
||||||
<button key={photo.id} type="button" aria-label={`Use photo ${index + 1} as banner`}
|
<button key={photo.id} type="button" aria-label={`Use photo ${index + 1} as banner`}
|
||||||
aria-pressed={formBanner === photo.id} onClick={() => { setFormBanner(photo.id); setFormCustomBanner(null); }}
|
aria-pressed={formBanner === photo.id} onClick={() => { setFormBanner(photo.id); setFormCustomBanner(null); }}
|
||||||
className="relative aspect-[3/2] overflow-hidden rounded-lg border-2 border-transparent aria-pressed:border-primary focus-visible:outline-2 focus-visible:outline-ring">
|
className="relative block aspect-[8/3] w-full min-w-0 overflow-hidden rounded-lg border-2 border-transparent aria-pressed:border-primary focus-visible:outline-2 focus-visible:outline-ring">
|
||||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
<img src={photo.thumbUrl ?? photo.displayUrl ?? ""} alt="" width={300} height={200} loading="lazy" className="size-full object-cover" />
|
<img src={photo.thumbUrl ?? photo.displayUrl ?? ""} alt="" width={320} height={120} loading="lazy" className="absolute inset-0 size-full object-cover" />
|
||||||
{formBanner === photo.id ? <span className="absolute right-2 top-2 rounded-full bg-primary p-1 text-primary-foreground"><CheckIcon aria-hidden="true" className="size-4" /></span> : null}
|
{formBanner === photo.id ? <span className="absolute right-2 top-2 rounded-full bg-primary p-1 text-primary-foreground"><CheckIcon aria-hidden="true" className="size-4" /></span> : null}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
</div>
|
||||||
{photos.isLoading ? <p className="text-xs text-muted-foreground">Loading photos…</p> : null}
|
{photos.isLoading ? <p className="text-xs text-muted-foreground">Loading photos…</p> : null}
|
||||||
{photos.isError ? <p role="alert" className="text-sm text-destructive">Could not load banner photos. Try reopening Settings.</p> : null}
|
{photos.isError ? <p role="alert" className="text-sm text-destructive">Could not load banner photos. Try reopening Settings.</p> : null}
|
||||||
{!photos.isLoading && !photos.isError && bannerPhotos.length === 0 ? <p className="text-sm text-muted-foreground">Upload and approve an event photo to use it as a banner.</p> : null}
|
{!photos.isLoading && !photos.isError && bannerPhotos.length === 0 ? <p className="text-sm text-muted-foreground">Upload and approve an event photo to use it as a banner.</p> : null}
|
||||||
</details>
|
</details>
|
||||||
<FieldDescription>Dedicated uploads stay out of the gallery. Gallery banners follow gallery visibility rules. Images are center-cropped for this preview; page layouts may crop differently.</FieldDescription>
|
<FieldDescription>Banners use an 8:3 frame everywhere. Uploads use your chosen crop and stay out of the gallery. Gallery photos are center-cropped and follow gallery visibility rules.</FieldDescription>
|
||||||
</Field>
|
</Field>
|
||||||
<Field>
|
<Field>
|
||||||
<FieldLabel htmlFor="notes-policy">Publish notes</FieldLabel>
|
<FieldLabel htmlFor="notes-policy">Publish notes</FieldLabel>
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { QrCodeIcon } from "lucide-react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import { createServerCaller } from "@/trpc/server";
|
import { createServerCaller } from "@/trpc/server";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { EventWorkspace, type EventWorkspaceTab } from "@/components/event-workspace";
|
import { EventWorkspace, type EventWorkspaceTab } from "@/components/event-workspace";
|
||||||
@@ -132,10 +135,11 @@ export default async function EventDashboardPage({
|
|||||||
<Badge variant="outline">Gallery held</Badge>
|
<Badge variant="outline">Gallery held</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="truncate text-sm text-muted-foreground">{event.guestUrl}</p>
|
<p className="truncate text-sm text-muted-foreground">{event.guestUrl.replace(/^https?:\/\//, "")}</p>
|
||||||
{event.location ? <p className="text-sm text-muted-foreground">{event.location}</p> : null}
|
{event.location ? <p className="text-sm text-muted-foreground">{event.location}</p> : null}
|
||||||
</div>
|
</div>
|
||||||
<CopyGuestLink url={event.guestUrl} />
|
<CopyGuestLink url={event.guestUrl} />
|
||||||
|
<Button asChild variant="outline"><Link href={`/dashboard/events/${event.id}/sign`}><QrCodeIcon data-icon="inline-start" />Guest sign</Link></Button>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
tabs={tabs}
|
tabs={tabs}
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useRef, useState } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Field, FieldDescription, FieldLabel } from "@/components/ui/field";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
|
import type { SignDesign } from "@/lib/guest-sign";
|
||||||
|
|
||||||
|
const presets = {
|
||||||
|
linenCard: { layout: "compact", paper: "card6x4", background: "#f1eadf", accent: "#473a30", decoration: "minimal", font: "funnel" },
|
||||||
|
indigo: { layout: "typography", background: "#4055b5", accent: "#ffffff", decoration: "minimal", font: "funnel" },
|
||||||
|
linen: { layout: "photo", background: "#f1eadf", accent: "#473a30", decoration: "minimal", font: "funnel" },
|
||||||
|
forest: { layout: "photo", background: "#153e35", accent: "#ffffff", decoration: "minimal", font: "funnel" },
|
||||||
|
mono: { layout: "typography", background: "#f4f2ec", accent: "#171717", decoration: "minimal", font: "funnel" },
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export function SignDesignControls({ design, onChange, onBusy }: { design: SignDesign; onChange: (patch: Partial<SignDesign>) => void; onBusy: (busy: boolean) => void }) {
|
||||||
|
const [busy, setBusy] = useState(false);
|
||||||
|
const [preset, setPreset] = useState("indigo");
|
||||||
|
const uploadId = useRef(0);
|
||||||
|
async function upload(file: File, field: "logoImage" | "backgroundImage") {
|
||||||
|
if (!["image/jpeg", "image/png", "image/webp"].includes(file.type) || !file.size || file.size > 10 * 1024 * 1024) {
|
||||||
|
toast.error("Choose a JPEG, PNG, or WebP up to 10 MB."); return;
|
||||||
|
}
|
||||||
|
const id = ++uploadId.current;
|
||||||
|
setBusy(true); onBusy(true);
|
||||||
|
const url = URL.createObjectURL(file);
|
||||||
|
try {
|
||||||
|
const image = new Image();
|
||||||
|
image.src = url;
|
||||||
|
await image.decode();
|
||||||
|
const max = field === "logoImage" ? 1000 : 2200;
|
||||||
|
const scale = Math.min(1, max / Math.max(image.naturalWidth, image.naturalHeight));
|
||||||
|
const canvas = document.createElement("canvas");
|
||||||
|
canvas.width = Math.max(1, Math.round(image.naturalWidth * scale)); canvas.height = Math.max(1, Math.round(image.naturalHeight * scale));
|
||||||
|
const context = canvas.getContext("2d");
|
||||||
|
if (!context) throw new Error("Image tools unavailable");
|
||||||
|
context.drawImage(image, 0, 0, canvas.width, canvas.height);
|
||||||
|
if (id === uploadId.current) onChange({ [field]: canvas.toDataURL("image/png") });
|
||||||
|
} catch { toast.error("Couldn't open this image. Try another file."); }
|
||||||
|
finally { URL.revokeObjectURL(url); setBusy(false); onBusy(false); }
|
||||||
|
}
|
||||||
|
return <>
|
||||||
|
{design.paper === "custom" ? <>
|
||||||
|
<Field><FieldLabel htmlFor="sign-unit">Units</FieldLabel><Select value={design.unit ?? "in"} onValueChange={value => {
|
||||||
|
const unit = value as "in" | "mm";
|
||||||
|
const factor = unit === design.unit ? 1 : unit === "mm" ? 25.4 : 1 / 25.4;
|
||||||
|
onChange({ unit, customWidth: Number(((design.customWidth ?? 8.5) * factor).toFixed(2)), customHeight: Number(((design.customHeight ?? 11) * factor).toFixed(2)) });
|
||||||
|
}}><SelectTrigger id="sign-unit" className="w-full"><SelectValue /></SelectTrigger><SelectContent><SelectGroup><SelectItem value="in">Inches</SelectItem><SelectItem value="mm">Millimeters</SelectItem></SelectGroup></SelectContent></Select></Field>
|
||||||
|
<div className="grid grid-cols-2 gap-3">{(["customWidth", "customHeight"] as const).map((key, i) => <Field key={key}><FieldLabel htmlFor={`sign-${key}`}>{i === 0 ? "Width" : "Height"}</FieldLabel><Input id={`sign-${key}`} type="number" step="0.01" min={design.unit === "mm" ? 76.2 : 3} max={design.unit === "mm" ? 1219.2 : 48} value={design[key] ?? ""} onChange={e => onChange({ [key]: e.target.value === "" ? 0 : Number(e.target.value) })} /></Field>)}</div>
|
||||||
|
<FieldDescription>Portrait, square, or landscape. 3–48 inches per edge. Small signs should be scanned up close.</FieldDescription>
|
||||||
|
</> : null}
|
||||||
|
<Field><FieldLabel htmlFor="sign-preset">Design preset</FieldLabel><Select value={preset} onValueChange={value => { setPreset(value); onChange(presets[value as keyof typeof presets]); }}><SelectTrigger id="sign-preset" className="w-full"><SelectValue /></SelectTrigger><SelectContent><SelectGroup><SelectItem value="linenCard">Card · Linen (no photo)</SelectItem><SelectItem value="indigo">Typography · Indigo</SelectItem><SelectItem value="linen">Photo · Linen</SelectItem><SelectItem value="forest">Photo · Forest</SelectItem><SelectItem value="mono">Typography · Paper</SelectItem></SelectGroup></SelectContent></Select></Field>
|
||||||
|
<Field><FieldLabel htmlFor="sign-font">Heading font</FieldLabel><Select value={design.font ?? "funnel"} onValueChange={font => onChange({ font: font as SignDesign["font"] })}><SelectTrigger id="sign-font" className="w-full"><SelectValue /></SelectTrigger><SelectContent><SelectGroup><SelectItem value="funnel">Funnel Display</SelectItem><SelectItem value="geologica">Geologica</SelectItem><SelectItem value="inter">Inter</SelectItem></SelectGroup></SelectContent></Select></Field>
|
||||||
|
<FieldDescription>{design.layout === "compact" ? "The Linen text-and-QR design without a photo. Starts at 6 × 4 inches; choose any paper size above." : design.layout === "photo" ? "A large event photo with a compact QR section. Add a photo below to enable sign export." : "Bold typography, a full-color background, and a prominent QR. Your uploaded photo is kept for photo presets."}</FieldDescription>
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<Field><FieldLabel htmlFor="sign-background">Background</FieldLabel><Input id="sign-background" type="color" value={design.background ?? "#eeece5"} onChange={e => onChange({ background: e.target.value })} /></Field>
|
||||||
|
<Field><FieldLabel htmlFor="sign-accent">Accent</FieldLabel><Input id="sign-accent" type="color" value={design.accent ?? "#4055b5"} onChange={e => onChange({ accent: e.target.value })} /><FieldDescription>Text adapts for contrast against the background.</FieldDescription></Field>
|
||||||
|
</div>
|
||||||
|
{([{ key: "backgroundImage", label: "Background photo" }, { key: "logoImage", label: "Your logo" }] as const).map(field => <Field key={field.key}>
|
||||||
|
<FieldLabel htmlFor={`sign-${field.key}`}>{field.label}</FieldLabel><Input id={`sign-${field.key}`} type="file" accept="image/jpeg,image/png,image/webp" disabled={busy} onChange={e => { const file = e.target.files?.[0]; e.target.value = ""; if (file) void upload(file, field.key); }} />
|
||||||
|
{design[field.key] ? <Button type="button" variant="outline" disabled={busy} onClick={() => onChange({ [field.key]: undefined })}>Remove {field.label.toLowerCase()}</Button> : null}
|
||||||
|
<FieldDescription>{field.key === "logoImage" ? "Transparent PNG works best. Replaces the Manyangles logo." : "Used by photo presets. Cropped to fill the photo area; switch presets without losing your upload."} Embedded in your export; never added to the event gallery.</FieldDescription>
|
||||||
|
</Field>)}
|
||||||
|
<Field><FieldLabel htmlFor="sign-brand">Manyangles logo</FieldLabel><Select value={design.showBrand === false ? "hide" : "show"} onValueChange={value => onChange({ showBrand: value === "show" })}><SelectTrigger id="sign-brand" className="w-full"><SelectValue /></SelectTrigger><SelectContent><SelectGroup><SelectItem value="show">Show when no custom logo</SelectItem><SelectItem value="hide">Hide</SelectItem></SelectGroup></SelectContent></Select></Field>
|
||||||
|
</>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { notFound } from "next/navigation";
|
||||||
|
import { createServerCaller } from "@/trpc/server";
|
||||||
|
import { effectiveEvent } from "@/lib/event-lifecycle";
|
||||||
|
import { signQr } from "@/server/sign-qr";
|
||||||
|
import { GuestSignStudio } from "./studio";
|
||||||
|
|
||||||
|
export default async function GuestSignPage({ params }: { params: Promise<{ id: string }> }) {
|
||||||
|
const { id } = await params;
|
||||||
|
const caller = await createServerCaller();
|
||||||
|
let event;
|
||||||
|
try { event = await caller.manager.event({ eventId: id }); } catch { notFound(); }
|
||||||
|
const current = effectiveEvent(event);
|
||||||
|
return <GuestSignStudio eventId={event.id} title={event.title} guestUrl={event.guestUrl} qr={signQr(event.guestUrl)}
|
||||||
|
warning={current.status === "draft" ? "This event is not public yet. Publish it before guests scan this sign." : !current.uploadEnabled ? "Submissions are currently closed. Enable uploads or check the event schedule before using this sign." : null} />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { ArrowLeftIcon, DownloadIcon, PrinterIcon, QrCodeIcon } from "lucide-react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Field, FieldDescription, FieldGroup, FieldLabel } from "@/components/ui/field";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
|
import { guestQrSvg, guestSignSvg, signPrintHtml, signPaper, SIGN_PAPERS, type SignDesign, type SignQr, type SignPaper } from "@/lib/guest-sign";
|
||||||
|
import { SignDesignControls } from "./design-controls";
|
||||||
|
|
||||||
|
export function GuestSignStudio({ eventId, title, guestUrl, qr, warning }: {
|
||||||
|
eventId: string; title: string; guestUrl: string; qr: SignQr; warning: string | null;
|
||||||
|
}) {
|
||||||
|
const [design, setDesign] = useState<SignDesign>({ title, headline: "Share your favorite moments", message: "Help us see the day through your eyes. Add your photos to our shared album.", paper: "letter", ink: "indigo", font: "funnel", layout: "typography", background: "#4055b5", accent: "#ffffff", decoration: "minimal", showBrand: true, customWidth: 8.5, customHeight: 11, unit: "in" });
|
||||||
|
const [fontError, setFontError] = useState(false);
|
||||||
|
const [assetBusy, setAssetBusy] = useState(false);
|
||||||
|
useEffect(() => {
|
||||||
|
let active = true;
|
||||||
|
void Promise.all(["funnel-display", "inter", "geologica"].map(async name => {
|
||||||
|
const response = await fetch(`/fonts/${name}.woff2`);
|
||||||
|
if (!response.ok) throw new Error("Font unavailable");
|
||||||
|
const bytes = new Uint8Array(await response.arrayBuffer());
|
||||||
|
let binary = "";
|
||||||
|
for (const byte of bytes) binary += String.fromCharCode(byte);
|
||||||
|
return `data:font/woff2;base64,${btoa(binary)}`;
|
||||||
|
})).then(([fontData, interFontData, geologicaFontData]) => {
|
||||||
|
if (active) setDesign(d => ({ ...d, fontData, interFontData, geologicaFontData }));
|
||||||
|
}).catch(() => { if (active) setFontError(true); });
|
||||||
|
return () => { active = false; };
|
||||||
|
}, []);
|
||||||
|
const printFrame = useRef<HTMLIFrameElement | null>(null);
|
||||||
|
const [printing, setPrinting] = useState(false);
|
||||||
|
useEffect(() => () => { printFrame.current?.remove(); }, []);
|
||||||
|
let sizeError: string | null = null;
|
||||||
|
let paper = SIGN_PAPERS.letter as ReturnType<typeof signPaper>;
|
||||||
|
try { paper = signPaper(design); } catch (error) { sizeError = error instanceof Error ? error.message : "Invalid paper size"; }
|
||||||
|
const svg = guestSignSvg(sizeError ? { ...design, paper: "letter" } : design, guestUrl, qr);
|
||||||
|
const fontReady = Boolean(design.fontData && design.interFontData && design.geologicaFontData);
|
||||||
|
const valid = Boolean(design.title.trim() && design.headline.trim() && design.message.trim() && !sizeError && fontReady && !assetBusy && (design.layout !== "photo" || design.backgroundImage));
|
||||||
|
const localUrl = ["localhost", "127.0.0.1", "[::1]"].includes(new URL(guestUrl).hostname);
|
||||||
|
function downloadSvg(content: string, filename: string) {
|
||||||
|
const url = URL.createObjectURL(new Blob([content], { type: "image/svg+xml;charset=utf-8" }));
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.href = url;
|
||||||
|
link.download = filename;
|
||||||
|
link.click();
|
||||||
|
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
||||||
|
}
|
||||||
|
function print() {
|
||||||
|
printFrame.current?.remove();
|
||||||
|
const frame = document.createElement("iframe");
|
||||||
|
frame.title = "Printable guest sign";
|
||||||
|
frame.setAttribute("aria-hidden", "true");
|
||||||
|
frame.tabIndex = -1;
|
||||||
|
frame.style.cssText = "position:fixed;left:-10000px;top:0;width:850px;height:1202px;border:0";
|
||||||
|
printFrame.current = frame;
|
||||||
|
setPrinting(true);
|
||||||
|
frame.onload = async () => {
|
||||||
|
try {
|
||||||
|
if (!frame.contentWindow) throw new Error("Print window unavailable");
|
||||||
|
await frame.contentDocument?.fonts.ready;
|
||||||
|
await Promise.all(Array.from(frame.contentDocument?.images ?? []).map(image => image.decode()));
|
||||||
|
frame.contentWindow.addEventListener("afterprint", () => frame.remove(), { once: true });
|
||||||
|
frame.contentWindow.focus();
|
||||||
|
frame.contentWindow.print();
|
||||||
|
} catch { toast.error("Printing is unavailable here. Download the SVG and open it in your browser to print."); }
|
||||||
|
finally { setPrinting(false); }
|
||||||
|
};
|
||||||
|
frame.srcdoc = signPrintHtml(svg, design);
|
||||||
|
document.body.append(frame);
|
||||||
|
}
|
||||||
|
return <div className="reveal flex min-w-0 flex-col gap-6 sm:gap-8">
|
||||||
|
<div className="flex flex-col gap-4 2xl:flex-row 2xl:items-end 2xl:justify-between">
|
||||||
|
<div className="flex min-w-0 flex-col gap-1">
|
||||||
|
<Link href={`/dashboard/events/${eventId}`} className="flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground"><ArrowLeftIcon className="size-4" />Back to event</Link>
|
||||||
|
<h1 className="text-3xl font-semibold tracking-tight sm:text-4xl">Guest sign studio</h1>
|
||||||
|
<p className="text-sm text-muted-foreground">A little sign. Everyone’s photos. Customize, preview, and print.</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<Button type="button" variant="outline" onClick={() => downloadSvg(guestQrSvg(qr), `manyangles-${eventId}-qr.svg`)}><QrCodeIcon data-icon="inline-start" />Download QR SVG</Button>
|
||||||
|
<Button variant="outline" disabled={!valid} onClick={() => downloadSvg(svg, `manyangles-${eventId}-guest-sign-${design.paper}.svg`)}><DownloadIcon data-icon="inline-start" />Download sign SVG</Button>
|
||||||
|
<Button disabled={!valid || printing} onClick={print}><PrinterIcon data-icon="inline-start" />{printing ? "Preparing…" : "Print / Save PDF"}</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid items-start gap-6 xl:grid-cols-[22rem_minmax(0,1fr)]">
|
||||||
|
<Card>
|
||||||
|
<CardHeader><CardTitle>Make it yours</CardTitle><CardDescription>Edits update the preview only, not your event. Download your sign before leaving.</CardDescription></CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<FieldGroup>
|
||||||
|
<Field><FieldLabel htmlFor="sign-paper">Paper size</FieldLabel>
|
||||||
|
<Select value={design.paper} onValueChange={paper => setDesign(d => ({ ...d, paper: paper as SignPaper }))}>
|
||||||
|
<SelectTrigger id="sign-paper" className="w-full"><SelectValue /></SelectTrigger>
|
||||||
|
<SelectContent><SelectGroup>{Object.entries(SIGN_PAPERS).map(([key, paper]) => <SelectItem key={key} value={key}>{paper.label}</SelectItem>)}<SelectItem value="custom">Custom dimensions</SelectItem></SelectGroup></SelectContent>
|
||||||
|
</Select>
|
||||||
|
</Field>
|
||||||
|
<SignDesignControls design={design} onChange={patch => setDesign(d => ({ ...d, ...patch }))} onBusy={setAssetBusy} />
|
||||||
|
{sizeError ? <p role="alert" className="text-sm text-destructive">{sizeError} Preview uses Letter until corrected.</p> : null}
|
||||||
|
{!fontReady ? <p role="status" className="text-sm text-muted-foreground">{fontError ? "Our fonts could not load. Reload before exporting." : "Loading embedded brand fonts…"}</p> : null}
|
||||||
|
{([{ key: "title", label: "Event name", max: 120 }, { key: "headline", label: "Headline", max: 44 }, { key: "message", label: "Welcome message", max: 120 }] as const).map(field =>
|
||||||
|
<Field key={field.key} data-invalid={!design[field.key].trim()}><FieldLabel htmlFor={`sign-${field.key}`}>{field.label}</FieldLabel>
|
||||||
|
<Input id={`sign-${field.key}`} value={design[field.key]} maxLength={field.max} aria-invalid={!design[field.key].trim()}
|
||||||
|
onChange={e => setDesign(d => ({ ...d, [field.key]: e.target.value }))} />
|
||||||
|
</Field>)}
|
||||||
|
<Field><FieldLabel>QR destination</FieldLabel><p className="break-all text-sm">{guestUrl.replace(/^https?:\/\//, "")}</p><FieldDescription>Uses the saved guest link, not a temporary download URL. Changing the event slug later means reprinting the sign.</FieldDescription></Field>
|
||||||
|
<p role="status" className="text-sm text-muted-foreground">{localUrl ? "Local preview: this QR points to localhost and will not work on guests’ phones. Generate the final sign on your deployed site." : "Before printing a batch, scan one copy with your phone."}{warning ? ` ${warning}` : ""}</p>
|
||||||
|
<p className="text-xs text-muted-foreground">Print at 100% on the selected paper size, with browser headers and footers off. The sign includes a safe page margin and a clear border around the QR code.</p>
|
||||||
|
</FieldGroup>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<div className="min-w-0 rounded-xl border bg-muted p-4 sm:p-6 xl:sticky xl:top-24">
|
||||||
|
<div className="mb-4 flex justify-between text-xs text-muted-foreground"><span>Live print preview</span><span>{paper.width} × {paper.height}</span></div>
|
||||||
|
<img src={`data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`} alt={`Printable guest sign for ${design.title}, with a QR code and upload instructions`}
|
||||||
|
width={850} height={paper.viewHeight} className="mx-auto block h-auto max-h-[75dvh] w-full object-contain drop-shadow-lg" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import weddingExit from "@/assets/homepage/457677937.webp";
|
import gardenToast from "@/assets/homepage/2157148814.webp";
|
||||||
|
import guestSelfie from "@/assets/homepage/2157148813.webp";
|
||||||
|
import weddingGreetings from "@/assets/homepage/599131260.webp";
|
||||||
import guestTunnel from "@/assets/homepage/599129661.webp";
|
import guestTunnel from "@/assets/homepage/599129661.webp";
|
||||||
import firstDance from "@/assets/homepage/498647922.webp";
|
import firstDance from "@/assets/homepage/498647922.webp";
|
||||||
import toast from "@/assets/homepage/240365594.webp";
|
|
||||||
import guest from "@/assets/homepage/189193781.webp";
|
import guest from "@/assets/homepage/189193781.webp";
|
||||||
import reception from "@/assets/homepage/208516668.webp";
|
|
||||||
import {
|
import {
|
||||||
ArrowRightIcon,
|
ArrowRightIcon,
|
||||||
CheckCircle2Icon,
|
CheckCircle2Icon,
|
||||||
@@ -18,6 +18,7 @@ import { createServerCaller } from "@/trpc/server";
|
|||||||
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 { BrandMark } from "@/components/brand-mark";
|
import { BrandMark } from "@/components/brand-mark";
|
||||||
|
import { SiteFooter } from "@/components/site-footer";
|
||||||
import { cn, formatEventDate } from "@/lib/utils";
|
import { cn, formatEventDate } from "@/lib/utils";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -38,7 +39,7 @@ const features = [
|
|||||||
number: "02",
|
number: "02",
|
||||||
icon: ShieldCheckIcon,
|
icon: ShieldCheckIcon,
|
||||||
title: "You decide what’s public",
|
title: "You decide what’s public",
|
||||||
body: "Every photo lands in a private review queue before it reaches the shared gallery.",
|
body: "Approve photos before sharing, publish automatically, or keep the gallery private. You choose.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
number: "03",
|
number: "03",
|
||||||
@@ -51,10 +52,10 @@ const features = [
|
|||||||
const previewPhotos = [
|
const previewPhotos = [
|
||||||
{ src: guest, layout: "row-span-2", position: "50% 40%" },
|
{ src: guest, layout: "row-span-2", position: "50% 40%" },
|
||||||
{ src: firstDance, layout: "col-span-2", position: "50% 45%" },
|
{ src: firstDance, layout: "col-span-2", position: "50% 45%" },
|
||||||
{ src: toast, layout: "", position: "50% 50%" },
|
{ src: guestSelfie, layout: "", position: "50% 40%" },
|
||||||
{ src: guestTunnel, layout: "row-span-2", position: "50% 50%" },
|
{ src: guestTunnel, layout: "row-span-2", position: "50% 50%" },
|
||||||
{ src: reception, layout: "", position: "65% 50%" },
|
{ src: gardenToast, layout: "", position: "50% 35%" },
|
||||||
{ src: weddingExit, layout: "", position: "50% 50%" },
|
{ src: weddingGreetings, layout: "", position: "50% 50%" },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
function AlbumPreview() {
|
function AlbumPreview() {
|
||||||
@@ -84,10 +85,10 @@ function AlbumPreview() {
|
|||||||
src={photo.src}
|
src={photo.src}
|
||||||
alt=""
|
alt=""
|
||||||
fill
|
fill
|
||||||
sizes={index === 1 ? "(max-width: 640px) 60vw, 360px" : "(max-width: 640px) 30vw, 180px"}
|
sizes={index === 0 || index === 3 ? "(max-width: 640px) 80vw, 480px" : index === 1 ? "(max-width: 640px) 60vw, 360px" : "(max-width: 640px) 40vw, 240px"}
|
||||||
|
quality={85}
|
||||||
className="object-cover"
|
className="object-cover"
|
||||||
style={{ objectPosition: photo.position }}
|
style={{ objectPosition: photo.position }}
|
||||||
placeholder="blur"
|
|
||||||
loading="eager"
|
loading="eager"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -117,6 +118,7 @@ export default async function HomePage() {
|
|||||||
const demoEvent = listed[0];
|
const demoEvent = listed[0];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<main>
|
<main>
|
||||||
<section className="page-pad mx-auto grid min-h-[calc(100svh-4rem)] w-full max-w-6xl items-center gap-12 py-14 sm:py-20 lg:grid-cols-[0.88fr_1.12fr] lg:gap-16 lg:py-24">
|
<section className="page-pad mx-auto grid min-h-[calc(100svh-4rem)] w-full max-w-6xl items-center gap-12 py-14 sm:py-20 lg:grid-cols-[0.88fr_1.12fr] lg:gap-16 lg:py-24">
|
||||||
<div className="reveal flex max-w-xl flex-col gap-6">
|
<div className="reveal flex max-w-xl flex-col gap-6">
|
||||||
@@ -152,31 +154,6 @@ export default async function HomePage() {
|
|||||||
<AlbumPreview />
|
<AlbumPreview />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="border-y border-border/80 bg-card/80 backdrop-blur-sm" aria-labelledby="how-it-works">
|
|
||||||
<div className="page-pad mx-auto w-full max-w-6xl py-14 sm:py-20">
|
|
||||||
<p className="text-xs font-bold tracking-[0.14em] text-primary uppercase">The whole handoff, handled</p>
|
|
||||||
<h2 id="how-it-works" className="mt-3 max-w-2xl text-4xl font-semibold tracking-[-0.035em] sm:text-5xl">
|
|
||||||
Built for the photos that never make it out of the group chat.
|
|
||||||
</h2>
|
|
||||||
<div className="mt-8 grid gap-3 md:grid-cols-3">
|
|
||||||
{features.map((feature) => {
|
|
||||||
const Icon = feature.icon;
|
|
||||||
return (
|
|
||||||
<Card key={feature.title} className="feature-panel overflow-hidden bg-background/60 shadow-none">
|
|
||||||
<CardHeader>
|
|
||||||
<div className="mb-3 flex items-center justify-between">
|
|
||||||
<span className="flex size-10 items-center justify-center rounded-lg bg-primary text-primary-foreground"><Icon aria-hidden="true" /></span>
|
|
||||||
<span className="font-mono text-xs font-bold tracking-widest text-muted-foreground">{feature.number}</span>
|
|
||||||
</div>
|
|
||||||
<CardTitle className="text-xl font-semibold">{feature.title}</CardTitle>
|
|
||||||
<CardDescription className="leading-6">{feature.body}</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{listed.length > 0 ? (
|
{listed.length > 0 ? (
|
||||||
<section className="page-pad mx-auto flex w-full max-w-6xl flex-col gap-6 py-14 sm:py-20">
|
<section className="page-pad mx-auto flex w-full max-w-6xl flex-col gap-6 py-14 sm:py-20">
|
||||||
@@ -208,11 +185,35 @@ export default async function HomePage() {
|
|||||||
</section>
|
</section>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<footer className="border-t border-border/80 bg-foreground text-background">
|
<section className="border-t bg-card/60" aria-labelledby="how-it-works">
|
||||||
<div className="page-pad mx-auto flex w-full max-w-6xl items-center justify-between gap-4 py-7 text-xs text-background/65">
|
<div className="page-pad mx-auto grid w-full max-w-6xl gap-10 py-14 sm:py-20 lg:grid-cols-2 lg:items-center lg:gap-16">
|
||||||
<span className="font-heading text-lg font-semibold text-background">Manyangles</span><span>Keep the moment, not the compression.</span>
|
<div className="relative overflow-hidden rounded-2xl">
|
||||||
|
<Image src={gardenToast} alt="Wedding guests sharing a champagne toast with the couple in a garden"
|
||||||
|
width={1000} height={1200} quality={85} sizes="(min-width: 1024px) 520px, 100vw"
|
||||||
|
className="aspect-[5/4] w-full object-cover lg:aspect-[5/6]" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-bold tracking-[0.14em] text-primary uppercase">Less chasing. More remembering.</p>
|
||||||
|
<h2 id="how-it-works" className="mt-3 scroll-mt-24 text-4xl font-semibold tracking-[-0.035em] sm:text-5xl">The day flies by.<br />The photos stay together.</h2>
|
||||||
|
<p className="mt-5 text-lg leading-7 text-muted-foreground">From the front row to the dance floor, everyone sees something different. Bring it all home.</p>
|
||||||
|
<ol className="mt-8 flex flex-col divide-y">
|
||||||
|
{features.map((feature) => {
|
||||||
|
const Icon = feature.icon;
|
||||||
|
return <li key={feature.number} className="flex gap-4 py-5 first:pt-0">
|
||||||
|
<span className="flex size-11 shrink-0 items-center justify-center rounded-full border bg-background text-primary"><Icon className="size-5" aria-hidden="true" /></span>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold"><span className="mr-2 font-mono text-xs text-muted-foreground">{feature.number}</span>{feature.title}</h3>
|
||||||
|
<p className="mt-1 text-sm leading-6 text-muted-foreground">{feature.body}</p>
|
||||||
|
</div>
|
||||||
|
</li>;
|
||||||
|
})}
|
||||||
|
</ol>
|
||||||
|
<Button asChild size="lg" className="mt-5"><Link href="/sign-up">Start your shared album<ArrowRightIcon data-icon="inline-end" aria-hidden="true" /></Link></Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
<SiteFooter />
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 414 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 154 KiB |
|
After Width: | Height: | Size: 298 KiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 117 KiB |
|
Before Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 329 KiB |
|
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 305 KiB |
|
After Width: | Height: | Size: 370 KiB |
@@ -9,9 +9,13 @@ and must not be redistributed as standalone stock assets.
|
|||||||
- 189193781: candid wedding guest
|
- 189193781: candid wedding guest
|
||||||
- 208516668: reception setting
|
- 208516668: reception setting
|
||||||
- 240365594: champagne toast
|
- 240365594: champagne toast
|
||||||
- 457677937: wedding ceremony exit
|
|
||||||
- 498647922: first dance
|
- 498647922: first dance
|
||||||
- 599129661: guests' wedding exit tunnel
|
- 599129661: guests' wedding exit tunnel
|
||||||
|
- 599131260: newlyweds greeting friends and family
|
||||||
|
- 2157148813: friends taking a garden selfie
|
||||||
|
- 2157148814: wedding guests sharing an outdoor toast
|
||||||
|
|
||||||
Source pages: `https://stock.adobe.com/images/{asset-id}`.
|
Source pages: `https://stock.adobe.com/images/{asset-id}`.
|
||||||
Renditions: auto-oriented, metadata stripped, bounded to 1000px, WebP quality 80.
|
Renditions: auto-oriented, metadata stripped, bounded to 2000px wide, WebP quality 90.
|
||||||
|
Responsive homepage images use Next.js quality 85; tall collage tiles request
|
||||||
|
enough source pixels for their height-driven cover crop on high-density screens.
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ export function BackendShell({ children, area, groups, activeGroupId }: {
|
|||||||
const activeItem = activeNavigationItem(workspaces, pathname);
|
const activeItem = activeNavigationItem(workspaces, pathname);
|
||||||
const activeWorkspace = workspaces.find((workspace) => workspace.items.includes(activeItem!)) ?? workspaces[0]!;
|
const activeWorkspace = workspaces.find((workspace) => workspace.items.includes(activeItem!)) ?? workspaces[0]!;
|
||||||
const activeGroup = groups.find((group) => group.id === activeGroupId) ?? groups[0];
|
const activeGroup = groups.find((group) => group.id === activeGroupId) ?? groups[0];
|
||||||
|
const signEventId = pathname.match(/^\/dashboard\/events\/([^/]+)\/sign$/)?.[1];
|
||||||
return (
|
return (
|
||||||
<div className="min-h-[calc(100dvh-4rem)] bg-background">
|
<div className="min-h-[calc(100dvh-4rem)] bg-background">
|
||||||
<DoubleSidebarNavigation key={area} workspaces={workspaces} activeItem={activeItem}
|
<DoubleSidebarNavigation key={area} workspaces={workspaces} activeItem={activeItem}
|
||||||
@@ -42,7 +43,7 @@ export function BackendShell({ children, area, groups, activeGroupId }: {
|
|||||||
<Link href={area === "platform" ? "/admin" : "/dashboard"} className="hover:text-foreground">{applicationLabel}</Link>
|
<Link href={area === "platform" ? "/admin" : "/dashboard"} className="hover:text-foreground">{applicationLabel}</Link>
|
||||||
<ChevronRight aria-hidden="true" className="size-3" />
|
<ChevronRight aria-hidden="true" className="size-3" />
|
||||||
<span>{activeWorkspace.label}</span>
|
<span>{activeWorkspace.label}</span>
|
||||||
{activeItem && activeItem.label !== activeWorkspace.label ? <><ChevronRight aria-hidden="true" className="size-3" /><span>{activeItem.label}</span></> : null}
|
{signEventId ? <><ChevronRight aria-hidden="true" className="size-3" /><Link href={`/dashboard/events/${signEventId}`} className="hover:text-foreground">Event</Link><ChevronRight aria-hidden="true" className="size-3" /><span aria-current="page">Guest sign</span></> : activeItem && activeItem.label !== activeWorkspace.label ? <><ChevronRight aria-hidden="true" className="size-3" /><span>{activeItem.label}</span></> : null}
|
||||||
</nav>
|
</nav>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import ReactCrop, { centerCrop, makeAspectCrop, type PercentCrop } from "react-image-crop";
|
||||||
|
import "react-image-crop/dist/ReactCrop.css";
|
||||||
|
import { BANNER_ASPECT_RATIO, type BannerCrop } from "@album/contracts";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||||
|
|
||||||
|
export function BannerCropDialog({ file, onCancel, onConfirm }: {
|
||||||
|
file: File;
|
||||||
|
onCancel: () => void;
|
||||||
|
onConfirm: (crop: BannerCrop) => void;
|
||||||
|
}) {
|
||||||
|
const [src, setSrc] = useState<string>();
|
||||||
|
const [crop, setCrop] = useState<PercentCrop>();
|
||||||
|
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
|
||||||
|
const [error, setError] = useState<string>();
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
let url: string | undefined;
|
||||||
|
async function prepare() {
|
||||||
|
try {
|
||||||
|
let preview: Blob = file;
|
||||||
|
if (/image\/hei[cf]/.test(file.type) || /\.hei[cf]$/i.test(file.name)) {
|
||||||
|
const { heicTo } = await import("heic-to/csp");
|
||||||
|
preview = await heicTo({ blob: file, type: "image/jpeg", quality: 0.9 });
|
||||||
|
}
|
||||||
|
if (cancelled) return;
|
||||||
|
url = URL.createObjectURL(preview);
|
||||||
|
setSrc(url);
|
||||||
|
} catch {
|
||||||
|
if (!cancelled) setError("This image couldn't be opened. Try a JPEG, PNG, or WebP copy.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void prepare();
|
||||||
|
return () => { cancelled = true; if (url) URL.revokeObjectURL(url); };
|
||||||
|
}, [file]);
|
||||||
|
function reset(width = dimensions.width, height = dimensions.height) {
|
||||||
|
setCrop(centerCrop(makeAspectCrop({ unit: "%", width: 100 }, BANNER_ASPECT_RATIO, width, height), width, height));
|
||||||
|
}
|
||||||
|
const valid = crop && crop.width * dimensions.width / 100 >= 8 && crop.height * dimensions.height / 100 >= 3 && !error;
|
||||||
|
return (
|
||||||
|
<Dialog open onOpenChange={(open) => { if (!open) onCancel(); }}>
|
||||||
|
<DialogContent className="max-h-[90dvh] overflow-y-auto sm:max-w-3xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Crop your banner</DialogTitle>
|
||||||
|
<DialogDescription>Drag to reposition or resize the 8:3 frame. The shaded area will be cut off. Your original stays untouched.</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="flex min-h-32 justify-center overflow-hidden rounded-lg bg-muted">
|
||||||
|
{src && !error ? <ReactCrop crop={crop} aspect={BANNER_ASPECT_RATIO} onChange={(_, percent) => setCrop(percent)} keepSelection ruleOfThirds>
|
||||||
|
<img src={src} alt="Banner crop preview" className="block max-h-[50dvh] max-w-full object-contain"
|
||||||
|
onLoad={(event) => {
|
||||||
|
const { naturalWidth: width, naturalHeight: height } = event.currentTarget;
|
||||||
|
setDimensions({ width, height });
|
||||||
|
reset(width, height);
|
||||||
|
}} onError={() => setError("This image couldn't be opened. Choose another image.")} />
|
||||||
|
</ReactCrop> : <p role={error ? "alert" : "status"} className="self-center p-4 text-sm text-muted-foreground">{error ?? "Preparing image…"}</p>}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">The selected area becomes your banner everywhere. After uploading, use Save changes to publish it.</p>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button type="button" variant="ghost" disabled={!crop || Boolean(error)} onClick={() => reset()}>Reset crop</Button>
|
||||||
|
<Button type="button" variant="outline" onClick={onCancel}>Cancel</Button>
|
||||||
|
<Button type="button" disabled={!valid} onClick={() => { if (crop && valid) onConfirm({ x: crop.x, y: crop.y, width: crop.width, height: crop.height }); }}>Use crop & upload</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,12 +1,15 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { UploadIcon } from "lucide-react";
|
import { UploadIcon } from "lucide-react";
|
||||||
import { allowedImageTypeSchema, MAX_PHOTO_BYTES } from "@album/contracts";
|
import { allowedImageTypeSchema, MAX_PHOTO_BYTES, type BannerCrop } from "@album/contracts";
|
||||||
import { api } from "@/trpc/react";
|
import { api } from "@/trpc/react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
|
const BannerCropDialog = dynamic(() => import("./banner-crop-dialog").then((module) => module.BannerCropDialog), { ssr: false });
|
||||||
|
|
||||||
export function BannerUpload({ eventId, selectedId, onSelect, onBusyChange }: {
|
export function BannerUpload({ eventId, selectedId, onSelect, onBusyChange }: {
|
||||||
eventId: string;
|
eventId: string;
|
||||||
selectedId: string | null;
|
selectedId: string | null;
|
||||||
@@ -15,6 +18,7 @@ export function BannerUpload({ eventId, selectedId, onSelect, onBusyChange }: {
|
|||||||
}) {
|
}) {
|
||||||
const input = useRef<HTMLInputElement>(null);
|
const input = useRef<HTMLInputElement>(null);
|
||||||
const [uploading, setUploading] = useState(false);
|
const [uploading, setUploading] = useState(false);
|
||||||
|
const [cropFile, setCropFile] = useState<File | null>(null);
|
||||||
const [bannerId, setBannerId] = useState<string | null>(null);
|
const [bannerId, setBannerId] = useState<string | null>(null);
|
||||||
const [startedAt, setStartedAt] = useState<number | null>(null);
|
const [startedAt, setStartedAt] = useState<number | null>(null);
|
||||||
const [timedOut, setTimedOut] = useState(false);
|
const [timedOut, setTimedOut] = useState(false);
|
||||||
@@ -40,7 +44,7 @@ export function BannerUpload({ eventId, selectedId, onSelect, onBusyChange }: {
|
|||||||
const timer = setTimeout(() => setTimedOut(true), 120_000);
|
const timer = setTimeout(() => setTimedOut(true), 120_000);
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [startedAt]);
|
}, [startedAt]);
|
||||||
async function upload(file: File) {
|
async function upload(file: File, crop: BannerCrop) {
|
||||||
const mime = file.type || (/\.heic$/i.test(file.name) ? "image/heic" : /\.heif$/i.test(file.name) ? "image/heif" : "");
|
const mime = file.type || (/\.heic$/i.test(file.name) ? "image/heic" : /\.heif$/i.test(file.name) ? "image/heif" : "");
|
||||||
const parsed = allowedImageTypeSchema.safeParse(mime);
|
const parsed = allowedImageTypeSchema.safeParse(mime);
|
||||||
if (!parsed.success || file.size <= 0 || file.size > MAX_PHOTO_BYTES) {
|
if (!parsed.success || file.size <= 0 || file.size > MAX_PHOTO_BYTES) {
|
||||||
@@ -52,7 +56,7 @@ export function BannerUpload({ eventId, selectedId, onSelect, onBusyChange }: {
|
|||||||
setBannerId(null);
|
setBannerId(null);
|
||||||
setStartedAt(null);
|
setStartedAt(null);
|
||||||
try {
|
try {
|
||||||
const pending = await create.mutateAsync({ eventId, contentType: parsed.data, byteSize: file.size });
|
const pending = await create.mutateAsync({ eventId, contentType: parsed.data, byteSize: file.size, crop });
|
||||||
const response = await fetch(pending.uploadUrl, {
|
const response = await fetch(pending.uploadUrl, {
|
||||||
method: "PUT", body: file, headers: { "Content-Type": parsed.data }, signal: AbortSignal.timeout(120_000),
|
method: "PUT", body: file, headers: { "Content-Type": parsed.data }, signal: AbortSignal.timeout(120_000),
|
||||||
});
|
});
|
||||||
@@ -65,23 +69,34 @@ export function BannerUpload({ eventId, selectedId, onSelect, onBusyChange }: {
|
|||||||
} finally { setUploading(false); }
|
} finally { setUploading(false); }
|
||||||
}
|
}
|
||||||
const processing = Boolean(bannerId) && status.data?.status !== "failed" && !status.isError && !timedOut;
|
const processing = Boolean(bannerId) && status.data?.status !== "failed" && !status.isError && !timedOut;
|
||||||
useEffect(() => { onBusyChange(uploading || processing); }, [uploading, processing, onBusyChange]);
|
useEffect(() => { onBusyChange(uploading || processing || Boolean(cropFile)); }, [uploading, processing, cropFile, onBusyChange]);
|
||||||
useEffect(() => () => onBusyChange(false), [onBusyChange]);
|
useEffect(() => () => onBusyChange(false), [onBusyChange]);
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-3 rounded-lg border p-3">
|
<div className="flex flex-col gap-3 rounded-lg border p-3">
|
||||||
|
{cropFile ? <BannerCropDialog file={cropFile} onCancel={() => setCropFile(null)} onConfirm={(crop) => {
|
||||||
|
const file = cropFile;
|
||||||
|
setCropFile(null);
|
||||||
|
void upload(file, crop);
|
||||||
|
}} /> : null}
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<input ref={input} type="file" accept="image/jpeg,image/png,image/webp,image/heic,image/heif,.heic,.heif"
|
<input ref={input} type="file" accept="image/jpeg,image/png,image/webp,image/heic,image/heif,.heic,.heif"
|
||||||
className="hidden" aria-label="Upload a dedicated event banner"
|
className="hidden" aria-label="Upload a dedicated event banner"
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
const file = event.target.files?.[0];
|
const file = event.target.files?.[0];
|
||||||
event.target.value = "";
|
event.target.value = "";
|
||||||
if (file) void upload(file);
|
if (!file) return;
|
||||||
|
const mime = file.type || (/\.heic$/i.test(file.name) ? "image/heic" : /\.heif$/i.test(file.name) ? "image/heif" : "");
|
||||||
|
if (!allowedImageTypeSchema.safeParse(mime).success || file.size <= 0 || file.size > MAX_PHOTO_BYTES) {
|
||||||
|
toast.error("Choose a JPEG, PNG, WebP, or HEIC image up to 25 MB.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setCropFile(file);
|
||||||
}} />
|
}} />
|
||||||
<Button type="button" variant="outline" disabled={uploading || processing} onClick={() => input.current?.click()}>
|
<Button type="button" variant="outline" disabled={uploading || processing} onClick={() => input.current?.click()}>
|
||||||
<UploadIcon data-icon="inline-start" />{uploading ? "Uploading…" : processing ? "Preparing banner…" : selectedId ? "Replace uploaded banner" : "Upload a banner"}
|
<UploadIcon data-icon="inline-start" />{uploading ? "Uploading…" : processing ? "Preparing banner…" : selectedId ? "Replace uploaded banner" : "Upload a banner"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground">A separate image just for this event's header. It won't appear in the gallery. Up to 25 MB.</p>
|
<p className="text-xs text-muted-foreground">Crop to 8:3 before uploading. Originals are preserved; the display copy is optimized. Separate from the gallery. Up to 25 MB.</p>
|
||||||
{processing ? <p role="status" className="text-sm text-muted-foreground">Preparing your banner… You can keep editing while it processes.</p> : null}
|
{processing ? <p role="status" className="text-sm text-muted-foreground">Preparing your banner… You can keep editing while it processes.</p> : null}
|
||||||
{status.isError || status.data?.status === "failed" || timedOut ? <p role="alert" className="text-sm text-destructive">{timedOut ? "Processing is taking longer than expected. Your previous banner is unchanged; try another upload." : "Couldn't prepare this banner. Your previous banner is unchanged. Try uploading another image."}</p> : null}
|
{status.isError || status.data?.status === "failed" || timedOut ? <p role="alert" className="text-sm text-destructive">{timedOut ? "Processing is taking longer than expected. Your previous banner is unchanged; try another upload." : "Couldn't prepare this banner. Your previous banner is unchanged. Try uploading another image."}</p> : null}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ export function EventMap({ latitude, longitude, location }: {
|
|||||||
const url = `https://www.openstreetmap.org/?mlat=${latitude}&mlon=${longitude}#map=16/${latitude}/${longitude}`;
|
const url = `https://www.openstreetmap.org/?mlat=${latitude}&mlon=${longitude}#map=16/${latitude}/${longitude}`;
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<iframe title={`Map of ${location}`} src={openStreetMapEmbedUrl(latitude, longitude)}
|
<div className="isolate overflow-hidden rounded-xl border bg-muted [clip-path:inset(0_round_var(--radius-xl))]">
|
||||||
loading="lazy" referrerPolicy="no-referrer" className="h-64 w-full rounded-xl border bg-muted" />
|
<iframe title={`Map of ${location}`} src={openStreetMapEmbedUrl(latitude, longitude)}
|
||||||
|
loading="lazy" referrerPolicy="no-referrer" className="block h-64 w-full border-0" />
|
||||||
|
</div>
|
||||||
<div className="flex flex-wrap items-center justify-between gap-2 text-xs text-muted-foreground">
|
<div className="flex flex-wrap items-center justify-between gap-2 text-xs text-muted-foreground">
|
||||||
<a href="https://www.openstreetmap.org/copyright" target="_blank" rel="noreferrer" className="underline underline-offset-4">© OpenStreetMap contributors</a>
|
<a href="https://www.openstreetmap.org/copyright" target="_blank" rel="noreferrer" className="underline underline-offset-4">© OpenStreetMap contributors</a>
|
||||||
<a href={url} target="_blank" rel="noreferrer" className="font-semibold underline underline-offset-4">Open Larger Map</a>
|
<a href={url} target="_blank" rel="noreferrer" className="font-semibold underline underline-offset-4">Open Larger Map</a>
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
import { ArrowUpRightIcon } from "lucide-react";
|
||||||
|
import { BrandLockup } from "@/components/brand-mark";
|
||||||
|
|
||||||
|
const footerLink = "inline-flex min-h-11 items-center gap-2 rounded-sm text-sm text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-ring";
|
||||||
|
|
||||||
|
export function SiteFooter() {
|
||||||
|
return (
|
||||||
|
<footer className="border-t bg-muted/40">
|
||||||
|
<div className="page-pad mx-auto w-full max-w-6xl">
|
||||||
|
<div className="grid gap-10 py-12 sm:grid-cols-[1fr_auto] sm:gap-16 sm:py-16">
|
||||||
|
<div className="flex max-w-sm flex-col items-start gap-5">
|
||||||
|
<Link href="/" aria-label="Manyangles home" className="rounded-sm focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-ring">
|
||||||
|
<BrandLockup />
|
||||||
|
</Link>
|
||||||
|
<p className="text-sm leading-relaxed text-muted-foreground">One event. Every perspective.<br />A shared home for the moments everyone captured.</p>
|
||||||
|
</div>
|
||||||
|
<nav aria-label="Footer" className="grid grid-cols-2 gap-10 sm:gap-16">
|
||||||
|
<div>
|
||||||
|
<h2 className="mb-2 text-sm font-semibold">Gather your photos</h2>
|
||||||
|
<ul>
|
||||||
|
<li><Link href="/sign-up" className={footerLink}>Host an event</Link></li>
|
||||||
|
<li><Link href="/#how-it-works" className={footerLink}>How it works</Link></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 className="mb-2 text-sm font-semibold">Your albums</h2>
|
||||||
|
<ul>
|
||||||
|
<li><Link href="/sign-in" className={footerLink}>Sign in</Link></li>
|
||||||
|
<li><Link href="/dashboard" className={footerLink}>Open workspace</Link></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-6 border-t py-6 sm:flex-row sm:items-center sm:justify-between">
|
||||||
|
<p className="text-xs text-muted-foreground">© {new Date().getFullYear()} Hadlock Technologies LLC. All rights reserved.</p>
|
||||||
|
<a href="https://hadlock.tech" aria-label="Made by Hadlock Tech — visit hadlock.tech"
|
||||||
|
className="group inline-flex min-h-11 w-fit items-center gap-4 rounded-sm text-foreground focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-ring">
|
||||||
|
<span className="text-xs text-muted-foreground">Made by</span>
|
||||||
|
{/* Original Hadlock Tech vector; the mask follows the active theme. */}
|
||||||
|
<span aria-hidden="true" className="block h-7 w-[121px] bg-current [mask-image:url('/branding/hadlock-tech.svg')] [mask-repeat:no-repeat] [mask-position:center] [mask-size:contain]" />
|
||||||
|
<ArrowUpRightIcon aria-hidden="true" className="size-4 text-muted-foreground transition-colors group-hover:text-foreground" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -75,8 +75,8 @@ export function SiteHeaderBar({ links, primary, signedIn, user, groups = [], act
|
|||||||
<div className="ml-auto flex items-center gap-1">
|
<div className="ml-auto flex items-center gap-1">
|
||||||
{signedIn ? (
|
{signedIn ? (
|
||||||
<>
|
<>
|
||||||
<ApplicationSwitcher links={links} pathname={pathname} />
|
|
||||||
<ThemeToggle />
|
<ThemeToggle />
|
||||||
|
<ApplicationSwitcher links={links} pathname={pathname} />
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import { expect, test } from "bun:test";
|
||||||
|
import sharp from "sharp";
|
||||||
|
import jsQR from "jsqr";
|
||||||
|
import { signQr } from "../server/sign-qr";
|
||||||
|
import { SIGN_PAPERS, guestQrSvg, guestSignSvg, signLines, signPrintHtml, signPaper, type SignDesign } from "./guest-sign";
|
||||||
|
|
||||||
|
test("standalone QR export scans to the guest link and keeps its quiet zone", async () => {
|
||||||
|
const url = "https://ma.hadlock.tech/e/test-wedding";
|
||||||
|
const svg = guestQrSvg(signQr(url));
|
||||||
|
expect(svg).toContain('transform="translate(4 4)"');
|
||||||
|
expect(svg).toContain('fill="white"');
|
||||||
|
const { data, info } = await sharp(Buffer.from(svg)).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
||||||
|
expect(jsQR(new Uint8ClampedArray(data), info.width, info.height)?.data).toBe(url);
|
||||||
|
});
|
||||||
|
|
||||||
|
const design: SignDesign = { title: "Sam & Alex’s wedding", headline: "Share your favorite moments", message: "Add your photos to our shared album.", paper: "letter", ink: "indigo" };
|
||||||
|
test("Linen cards scan at every preset size with exact physical export dimensions", async () => {
|
||||||
|
const url = "https://ma.hadlock.tech/e/test-wedding";
|
||||||
|
for (const paper of Object.keys(SIGN_PAPERS) as (keyof typeof SIGN_PAPERS)[]) {
|
||||||
|
const svg = guestSignSvg({ ...design, paper, layout: "compact", background: "#f1eadf", accent: "#473a30" }, url, signQr(url));
|
||||||
|
const size = SIGN_PAPERS[paper];
|
||||||
|
expect(signPrintHtml(svg, paper)).toContain(`size:${size.width} ${size.height}`);
|
||||||
|
expect(svg).not.toContain("Add your event photo");
|
||||||
|
const { data, info } = await sharp(Buffer.from(svg), { density: 120 }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
||||||
|
expect(jsQR(new Uint8ClampedArray(data), info.width, info.height)?.data).toBe(url);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
test("photo and typography layouts scan in portrait, square, and landscape", async () => {
|
||||||
|
const url = "https://ma.hadlock.tech/e/test-wedding";
|
||||||
|
const photo = `data:image/png;base64,${(await sharp({ create: { width: 80, height: 80, channels: 3, background: '#ae8268' } }).png().toBuffer()).toString('base64')}`;
|
||||||
|
for (const layout of ["photo", "typography"] as const) {
|
||||||
|
for (const [customWidth, customHeight] of [[8.5, 11], [5, 5], [10, 7]]) {
|
||||||
|
const svg = guestSignSvg({ ...design, layout, paper: "custom", customWidth, customHeight, backgroundImage: photo }, url, signQr(url));
|
||||||
|
const { data, info } = await sharp(Buffer.from(svg), { density: 120 }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
||||||
|
expect(jsQR(new Uint8ClampedArray(data), info.width, info.height)?.data).toBe(url);
|
||||||
|
expect(svg.includes('<image')).toBe(layout === "photo");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
test("background is the page surface with contrasting text and an isolated white QR", () => {
|
||||||
|
const url = "https://ma.hadlock.tech/e/demo";
|
||||||
|
const svg = guestSignSvg({ ...design, background: "#204c40", accent: "#204c40" }, url, signQr(url));
|
||||||
|
expect(svg).toContain('font-family="SignInter, sans-serif" fill="#ffffff"');
|
||||||
|
expect(svg).not.toContain('rx="24" fill="white"');
|
||||||
|
expect(svg).toContain('<rect width="100%" height="100%" fill="white"/>');
|
||||||
|
const photo = guestSignSvg({ ...design, backgroundImage: "data:image/png;base64,AA==" }, url, signQr(url));
|
||||||
|
expect(photo).toContain('fill="black" opacity=".68"');
|
||||||
|
expect(photo).toContain('font-family="SignInter, sans-serif" fill="#ffffff"');
|
||||||
|
});
|
||||||
|
test("signs embed our font families and keep the wordmark in Geologica", () => {
|
||||||
|
const url = "https://ma.hadlock.tech/e/demo";
|
||||||
|
for (const font of ["funnel", "inter", "geologica"] as const) {
|
||||||
|
const svg = guestSignSvg({ ...design, font, fontData: "data:font/woff2;base64,AA==", interFontData: "data:font/woff2;base64,AA==", geologicaFontData: "data:font/woff2;base64,AA==" }, url, signQr(url));
|
||||||
|
for (const family of ["SignFunnel", "SignInter", "SignGeologica"]) expect(svg).toContain(`@font-face{font-family:${family};`);
|
||||||
|
expect(svg).toContain('font-family="SignInter, sans-serif"');
|
||||||
|
expect(svg).toContain('font-family="SignGeologica, sans-serif" font-size="26.5"');
|
||||||
|
const lockup = svg.slice(svg.indexOf('data-brand-lockup="manyangles"'));
|
||||||
|
expect(lockup).toContain('fill="currentColor"');
|
||||||
|
expect(lockup).toContain('stroke="currentColor"');
|
||||||
|
expect(lockup).not.toMatch(/(?:fill|stroke)="#/);
|
||||||
|
expect(svg).not.toMatch(/Arial|Georgia|Helvetica/);
|
||||||
|
}
|
||||||
|
const unsafe = guestSignSvg({ ...design, interFontData: "https://example.com/font.woff2" }, url, signQr(url));
|
||||||
|
expect(unsafe).not.toContain("https://example.com/font.woff2");
|
||||||
|
});
|
||||||
|
test("exported Letter and A4 signs scan to the complete guest URL", async () => {
|
||||||
|
const url = "https://ma.hadlock.tech/e/test-wedding";
|
||||||
|
for (const paper of ["letter", "a4"] as const) {
|
||||||
|
const svg = guestSignSvg({ ...design, paper }, url, signQr(url));
|
||||||
|
const { data, info } = await sharp(Buffer.from(svg), { density: 120 }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
||||||
|
expect(jsQR(new Uint8ClampedArray(data), info.width, info.height)?.data).toBe(url);
|
||||||
|
expect(svg).toContain('transform="translate(4 4)"');
|
||||||
|
expect(signPrintHtml(svg, paper)).toContain(paper === "letter" ? "size:8.5in 11in" : "size:210mm 297mm");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
test("custom square and landscape designs retain exact print size and scan", async () => {
|
||||||
|
const url = "https://ma.hadlock.tech/e/test-wedding";
|
||||||
|
for (const [customWidth, customHeight] of [[5, 5], [10, 7], [4, 6]]) {
|
||||||
|
const custom: SignDesign = { ...design, paper: "custom", customWidth, customHeight, unit: "in", decoration: "frame", background: "#204c40" };
|
||||||
|
const svg = guestSignSvg(custom, url, signQr(url));
|
||||||
|
const { data, info } = await sharp(Buffer.from(svg), { density: 120 }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
||||||
|
expect(jsQR(new Uint8ClampedArray(data), info.width, info.height)?.data).toBe(url);
|
||||||
|
expect(signPrintHtml(svg, custom)).toContain(`size:${customWidth}in ${customHeight}in`);
|
||||||
|
}
|
||||||
|
expect(signPaper({ paper: "custom", customWidth: 127, customHeight: 178, unit: "mm" }).width).toBe("127mm");
|
||||||
|
expect(() => signPaper({ paper: "custom", customWidth: 0, customHeight: 10 })).toThrow();
|
||||||
|
expect(() => signPaper({ paper: "custom", customWidth: 48, customHeight: 3 })).toThrow();
|
||||||
|
});
|
||||||
|
test("external and SVG image uploads cannot inject export content", () => {
|
||||||
|
const svg = guestSignSvg({ ...design, logoImage: 'data:image/svg+xml;<script>', backgroundImage: 'https://example.com/track', accent: '\"><script>' }, "https://ma.hadlock.tech/e/demo", signQr("https://ma.hadlock.tech/e/demo"));
|
||||||
|
expect(svg).not.toContain("<script>");
|
||||||
|
expect(svg).not.toContain("https://example.com/track");
|
||||||
|
});
|
||||||
|
test("custom text is escaped and long words wrap", () => {
|
||||||
|
const svg = guestSignSvg({ ...design, title: '<script>alert("x")</script>' }, "https://ma.hadlock.tech/e/demo", signQr("https://ma.hadlock.tech/e/demo"));
|
||||||
|
expect(svg).not.toContain("<script>");
|
||||||
|
expect(svg).toContain("<script>");
|
||||||
|
expect(signLines("a".repeat(120), 48).every(line => line.length <= 48)).toBe(true);
|
||||||
|
expect(() => signQr("javascript:alert(1)")).toThrow();
|
||||||
|
});
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
export const SIGN_PAPERS = {
|
||||||
|
letter: { label: "US Letter · 8.5 × 11 in", width: "8.5in", height: "11in", viewHeight: 1100 },
|
||||||
|
a4: { label: "A4 · 210 × 297 mm", width: "210mm", height: "297mm", viewHeight: 1202 },
|
||||||
|
letterLandscape: { label: "US Letter landscape · 11 × 8.5 in", width: "11in", height: "8.5in", viewHeight: 850 * 8.5 / 11 },
|
||||||
|
a4Landscape: { label: "A4 landscape · 297 × 210 mm", width: "297mm", height: "210mm", viewHeight: 850 * 210 / 297 },
|
||||||
|
a5: { label: "A5 · 148 × 210 mm", width: "148mm", height: "210mm", viewHeight: 850 * 210 / 148 },
|
||||||
|
a5Landscape: { label: "A5 landscape · 210 × 148 mm", width: "210mm", height: "148mm", viewHeight: 850 * 148 / 210 },
|
||||||
|
card3x5: { label: "Small card · 3 × 5 in", width: "3in", height: "5in", viewHeight: 850 * 5 / 3 },
|
||||||
|
card5x3: { label: "Small landscape card · 5 × 3 in", width: "5in", height: "3in", viewHeight: 510 },
|
||||||
|
card4x6: { label: "Postcard · 4 × 6 in", width: "4in", height: "6in", viewHeight: 1275 },
|
||||||
|
card6x4: { label: "Landscape postcard · 6 × 4 in", width: "6in", height: "4in", viewHeight: 850 * 4 / 6 },
|
||||||
|
card5x7: { label: "Table card · 5 × 7 in", width: "5in", height: "7in", viewHeight: 1190 },
|
||||||
|
card7x5: { label: "Landscape table card · 7 × 5 in", width: "7in", height: "5in", viewHeight: 850 * 5 / 7 },
|
||||||
|
square: { label: "Square card · 5 × 5 in", width: "5in", height: "5in", viewHeight: 850 },
|
||||||
|
} as const;
|
||||||
|
export type SignPaper = keyof typeof SIGN_PAPERS | "custom";
|
||||||
|
export type SignDesign = { title: string; headline: string; message: string; paper: SignPaper; ink: "indigo" | "black";
|
||||||
|
customWidth?: number; customHeight?: number; unit?: "in" | "mm";
|
||||||
|
layout?: "photo" | "typography" | "compact";
|
||||||
|
background?: string; accent?: string; font?: "funnel" | "geologica" | "inter";
|
||||||
|
decoration?: "arch" | "frame" | "minimal"; backgroundImage?: string; logoImage?: string;
|
||||||
|
showBrand?: boolean; fontData?: string; interFontData?: string; geologicaFontData?: string;
|
||||||
|
};
|
||||||
|
export type SignQr = { path: string; size: number };
|
||||||
|
|
||||||
|
export function guestQrSvg(qr: SignQr) {
|
||||||
|
const size = qr.size + 8;
|
||||||
|
return `<svg xmlns="http://www.w3.org/2000/svg" width="${size * 16}" height="${size * 16}" viewBox="0 0 ${size} ${size}" shape-rendering="crispEdges"><title>Manyangles guest album QR code</title><rect width="100%" height="100%" fill="white"/><path d="${qr.path}" transform="translate(4 4)" fill="black"/></svg>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function signPaper(design: Pick<SignDesign, "paper" | "customWidth" | "customHeight" | "unit">) {
|
||||||
|
if (design.paper !== "custom") return SIGN_PAPERS[design.paper];
|
||||||
|
const unit = design.unit === "mm" ? "mm" : "in";
|
||||||
|
const width = design.customWidth ?? 8.5, height = design.customHeight ?? 11;
|
||||||
|
const inches = unit === "mm" ? 25.4 : 1;
|
||||||
|
if (![width, height].every(n => Number.isFinite(n) && n / inches >= 3 && n / inches <= 48)) throw new Error("Choose dimensions between 3 and 48 inches (76.2–1219.2 mm).");
|
||||||
|
if (Math.max(width / height, height / width) > 2.5) throw new Error("Keep the long edge within 2.5 times the short edge so the sign stays readable.");
|
||||||
|
return { label: "Custom", width: `${width}${unit}`, height: `${height}${unit}`, viewHeight: 850 * height / width };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function escapeXml(value: string) {
|
||||||
|
return value.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]!);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hard-wrap even unbroken names/URLs; no user input becomes SVG markup.
|
||||||
|
export function signLines(value: string, limit: number) {
|
||||||
|
const words = value.trim().split(/\s+/).flatMap(word => word.match(new RegExp(`.{1,${limit}}`, "gu")) ?? []);
|
||||||
|
const lines: string[] = [];
|
||||||
|
for (const word of words) {
|
||||||
|
const last = lines.length - 1;
|
||||||
|
if (last >= 0 && `${lines[last]} ${word}`.length <= limit) lines[last] += ` ${word}`;
|
||||||
|
else lines.push(word);
|
||||||
|
}
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function guestSignSvg(design: SignDesign, guestUrl: string, qr: SignQr) {
|
||||||
|
const paper = signPaper(design);
|
||||||
|
const color = (value: string | undefined, fallback: string) => /^#[\da-f]{6}$/i.test(value ?? "") ? value! : fallback;
|
||||||
|
const ink = color(design.accent, design.ink === "black" ? "#171717" : "#4055b5");
|
||||||
|
const luminance = (hex: string) => {
|
||||||
|
const channels = [1, 3, 5].map(index => parseInt(hex.slice(index, index + 2), 16) / 255).map(c => c <= .04045 ? c / 12.92 : ((c + .055) / 1.055) ** 2.4);
|
||||||
|
return channels[0]! * .2126 + channels[1]! * .7152 + channels[2]! * .0722;
|
||||||
|
};
|
||||||
|
const background = color(design.background, "#eeece5");
|
||||||
|
const raster = (value?: string) => /^data:image\/(png|jpeg|webp);base64,[A-Za-z0-9+/=]+$/.test(value ?? "") ? value : undefined;
|
||||||
|
const photo = raster(design.backgroundImage), logo = raster(design.logoImage);
|
||||||
|
const surfaceLuminance = luminance(background);
|
||||||
|
const bodyInk = photo || surfaceLuminance < .18 ? "#ffffff" : "#171717";
|
||||||
|
const accentContrast = (Math.max(luminance(ink), surfaceLuminance) + .05) / (Math.min(luminance(ink), surfaceLuminance) + .05);
|
||||||
|
const headingInk = !photo && accentContrast >= 4.5 ? ink : bodyInk;
|
||||||
|
const landscape = paper.viewHeight < 850;
|
||||||
|
const w = landscape ? 1200 : 850, h = landscape ? 800 : 1100;
|
||||||
|
const scale = Math.min(850 / w, paper.viewHeight / h);
|
||||||
|
const cx = landscape ? 328 : 425;
|
||||||
|
const text = (value: string, y: number, size: number, limit: number, weight = 400, x = cx) => signLines(value, limit).map((line, i) =>
|
||||||
|
`<text x="${x}" y="${y + i * size * 1.25}" text-anchor="middle" font-size="${size}" font-weight="${weight}">${escapeXml(line)}</text>`).join("");
|
||||||
|
const headingFont = design.font === "geologica" ? "SignGeologica, sans-serif" : design.font === "inter" ? "SignInter, sans-serif" : "SignFunnel, sans-serif";
|
||||||
|
const fontFaces = [["SignFunnel", design.fontData], ["SignInter", design.interFontData], ["SignGeologica", design.geologicaFontData]]
|
||||||
|
.filter(([, data]) => /^data:font\/woff2;base64,[A-Za-z0-9+/=]+$/.test(data ?? ""))
|
||||||
|
.map(([family, data]) => `@font-face{font-family:${family};src:url('${data}') format('woff2');font-weight:100 900;font-style:normal}`).join("");
|
||||||
|
const qx = landscape ? 710 : 240, qy = landscape ? 235 : 420;
|
||||||
|
if (design.layout) {
|
||||||
|
const compact = design.layout === "compact";
|
||||||
|
const photoLed = design.layout === "photo";
|
||||||
|
const foreground = surfaceLuminance < .18 ? "#ffffff" : "#171717";
|
||||||
|
const headingColor = accentContrast >= 4.5 ? ink : foreground;
|
||||||
|
const left = landscape && photoLed ? 650 : 70;
|
||||||
|
const top = photoLed ? (landscape ? 80 : 600) : 85;
|
||||||
|
const column = photoLed ? (landscape ? 480 : 410) : (landscape ? 600 : 710);
|
||||||
|
const fontSize = compact ? 58 : photoLed ? 46 : (landscape ? 80 : 88);
|
||||||
|
const headlineLines = signLines(design.headline, compact ? 20 : photoLed ? 18 : 16);
|
||||||
|
const headlineSize = Math.min(fontSize, (photoLed ? 170 : 350) / (headlineLines.length * 1.1));
|
||||||
|
const lines = (value: string, x: number, y: number, size: number, limit: number, weight = 400) =>
|
||||||
|
signLines(value, limit).map((line, index) => `<text x="${x}" y="${y + index * size * 1.25}" font-size="${size}" font-weight="${weight}">${escapeXml(line)}</text>`).join("");
|
||||||
|
const qrSize = compact ? 320 : photoLed ? 240 : 340;
|
||||||
|
const qrX = landscape ? (photoLed ? 870 : 790) : (photoLed ? 540 : 440);
|
||||||
|
const qrY = compact ? (landscape ? 260 : 530) : landscape ? (photoLed ? 415 : 235) : (photoLed ? 755 : 605);
|
||||||
|
const messageY = top + (compact ? 310 : photoLed ? 250 : 445);
|
||||||
|
const brandMarkup = logo ? `<image href="${logo}" x="${cx - 100}" y="${landscape ? 700 : 1005}" width="200" height="48" preserveAspectRatio="xMidYMid meet"/>` : design.showBrand !== false ? `<g data-brand-lockup="manyangles" color="${headingColor}" fill="currentColor" transform="translate(${cx - 96} ${landscape ? 711 : 1016})"><svg width="32" height="32" viewBox="0 0 32 32" fill="none"><path d="M14 4H8a4 4 0 0 0-4 4v6M18 4h6a4 4 0 0 1 4 4v6M28 18v6a4 4 0 0 1-4 4h-6M14 28H8a4 4 0 0 1-4-4v-6" stroke="currentColor" stroke-width="3" stroke-linecap="round"/><circle cx="21" cy="11" r="2.25" fill="currentColor"/><path d="m8.5 22 5-6.2a1.7 1.7 0 0 1 2.6-.1l2.6 2.9 1.7-1.8a1.7 1.7 0 0 1 2.5 0l1.6 1.8" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></svg><text x="40" y="24" font-family="SignGeologica, sans-serif" font-size="26.5" font-weight="700" letter-spacing="-1.06" style="font-variation-settings:'SHRP' 70">Manyangles</text></g>` : "";
|
||||||
|
return `<svg xmlns="http://www.w3.org/2000/svg" width="${paper.width}" height="${paper.height}" viewBox="0 0 850 ${paper.viewHeight}">
|
||||||
|
<title>${escapeXml(design.title)} — guest photo sign</title>
|
||||||
|
<style>${fontFaces}</style>
|
||||||
|
<rect width="850" height="${paper.viewHeight}" fill="${background}"/>
|
||||||
|
<g transform="translate(${(850 - w * scale) / 2} ${(paper.viewHeight - h * scale) / 2}) scale(${scale})" font-family="SignInter, sans-serif" fill="${foreground}">
|
||||||
|
${photoLed ? `<svg width="${landscape ? 580 : w}" height="${landscape ? h : 530}" viewBox="0 0 ${landscape ? 580 : w} ${landscape ? h : 530}">
|
||||||
|
<rect width="100%" height="100%" fill="${ink}"/>
|
||||||
|
${photo ? `<image href="${photo}" width="100%" height="100%" preserveAspectRatio="xMidYMid slice"/>` : `<text x="50%" y="50%" text-anchor="middle" fill="white" font-size="22">Add your event photo</text>`}
|
||||||
|
</svg>` : ""}
|
||||||
|
${lines(design.title, left, top, compact ? 25 : 19, Math.floor(column / (compact ? 15 : 11)), 500)}
|
||||||
|
<g font-family="${headingFont}" fill="${headingColor}" letter-spacing="-1.8">
|
||||||
|
${headlineLines.map((line, index) => `<text x="${left}" y="${top + 80 + index * headlineSize * 1.1}" font-size="${headlineSize}" font-weight="650">${escapeXml(line)}</text>`).join("")}
|
||||||
|
</g>
|
||||||
|
${lines(design.message, left, messageY, compact ? 26 : 18, compact ? (landscape ? 38 : 24) : photoLed ? (landscape ? 42 : 35) : (landscape ? 48 : 32))}
|
||||||
|
<svg x="${qrX}" y="${qrY}" width="${qrSize}" height="${qrSize}" viewBox="0 0 ${qr.size + 8} ${qr.size + 8}" shape-rendering="crispEdges"><rect width="100%" height="100%" fill="white"/><path d="${qr.path}" transform="translate(4 4)" fill="black"/></svg>
|
||||||
|
${lines("Scan to share", qrX, qrY - 22, compact ? 24 : 18, 30, 600)}
|
||||||
|
${lines(guestUrl.replace(/^https?:\/\//, ""), qrX, qrY + qrSize + 24, compact ? 15 : 11, Math.floor(qrSize / (compact ? 9 : 7)))}
|
||||||
|
${!photoLed && !compact ? lines("Your photos. Our story.", left, landscape ? 665 : 850, 24, 30, 600) : ""}
|
||||||
|
${lines("No app or account needed.", left, photoLed ? (landscape ? 665 : 950) : (landscape ? 705 : 890), compact ? 22 : 16, photoLed && landscape ? 22 : 32)}
|
||||||
|
${lines("Photos and notes welcome.", left, photoLed ? (landscape ? 710 : 976) : (landscape ? 735 : 920), compact ? 22 : 16, photoLed && landscape ? 22 : 32)}
|
||||||
|
<g transform="translate(${(landscape ? (photoLed ? 650 : 790) : 70) - (cx - 96)} ${landscape ? (photoLed ? 39 : compact ? -21 : -626) : 20})">${brandMarkup}</g>
|
||||||
|
</g>
|
||||||
|
</svg>`;
|
||||||
|
}
|
||||||
|
return `<svg xmlns="http://www.w3.org/2000/svg" width="${paper.width}" height="${paper.height}" viewBox="0 0 850 ${paper.viewHeight}">
|
||||||
|
<title>${escapeXml(design.title)} — guest photo sign</title>
|
||||||
|
${fontFaces ? `<style>${fontFaces}</style>` : ""}
|
||||||
|
<rect width="850" height="${paper.viewHeight}" fill="${background}"/>
|
||||||
|
${photo ? `<image href="${photo}" width="850" height="${paper.viewHeight}" preserveAspectRatio="xMidYMid slice"/><rect width="850" height="${paper.viewHeight}" fill="black" opacity=".68"/>` : ""}
|
||||||
|
<g font-family="SignInter, sans-serif" fill="${bodyInk}" transform="translate(${(850 - w * scale) / 2} ${(paper.viewHeight - h * scale) / 2}) scale(${scale})">
|
||||||
|
${design.decoration !== "minimal" ? `<rect x="35" y="35" width="${w - 70}" height="${h - 70}" rx="${design.decoration === "arch" ? 24 : 0}" fill="none" stroke="${headingInk}" stroke-opacity=".3" stroke-width="1"/>` : ""}
|
||||||
|
<path d="M${landscape ? 90 : 85} 158h${landscape ? 470 : 680}" stroke="${headingInk}" stroke-opacity=".25" stroke-width="1"/>
|
||||||
|
<g font-family="${headingFont}">
|
||||||
|
${text(design.title, 108, 22, landscape ? 34 : 48, 500)}
|
||||||
|
<g fill="${headingInk}" letter-spacing="-1.5">${text(design.headline, landscape ? 265 : 230, 52, landscape ? 18 : 24, 600)}</g>
|
||||||
|
</g>
|
||||||
|
${text(design.message, landscape ? 485 : 359, 19, landscape ? 40 : 60)}
|
||||||
|
<svg x="${qx}" y="${qy}" width="370" height="370" viewBox="0 0 ${qr.size + 8} ${qr.size + 8}" shape-rendering="crispEdges">
|
||||||
|
<rect width="100%" height="100%" fill="white"/><path d="${qr.path}" transform="translate(4 4)" fill="black"/>
|
||||||
|
</svg>
|
||||||
|
${text("Scan with your phone camera", landscape ? 644 : 820, 20, 40, 600, qx + 185)}
|
||||||
|
${text(guestUrl.replace(/^https?:\/\//, ""), landscape ? 675 : 854, guestUrl.length > 160 ? 10 : 14, guestUrl.length > 160 ? (landscape ? 68 : 110) : (landscape ? 46 : 78), 400, qx + 185)}
|
||||||
|
<path d="M${landscape ? 90 : 85} ${landscape ? 575 : 901}h${landscape ? 470 : 680}" stroke="${headingInk}" stroke-opacity=".25" stroke-width="1"/>
|
||||||
|
${text("Scan. Choose your photos. Upload.", landscape ? 611 : 935, 21, 50, 500)}
|
||||||
|
${text("No app or account needed. Notes welcome, too.", landscape ? 644 : 965, 16, landscape ? 36 : 55)}
|
||||||
|
${logo ? `<image href="${logo}" x="${cx - 100}" y="${landscape ? 700 : 1005}" width="200" height="48" preserveAspectRatio="xMidYMid meet"/>` : design.showBrand !== false ? `<g data-brand-lockup="manyangles" color="${headingInk}" fill="currentColor" transform="translate(${cx - 96} ${landscape ? 711 : 1016})"><svg width="32" height="32" viewBox="0 0 32 32" fill="none"><path d="M14 4H8a4 4 0 0 0-4 4v6M18 4h6a4 4 0 0 1 4 4v6M28 18v6a4 4 0 0 1-4 4h-6M14 28H8a4 4 0 0 1-4-4v-6" stroke="currentColor" stroke-width="3" stroke-linecap="round"/><circle cx="21" cy="11" r="2.25" fill="currentColor"/><path d="m8.5 22 5-6.2a1.7 1.7 0 0 1 2.6-.1l2.6 2.9 1.7-1.8a1.7 1.7 0 0 1 2.5 0l1.6 1.8" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></svg><text x="40" y="24" font-family="SignGeologica, sans-serif" font-size="26.5" font-weight="700" letter-spacing="-1.06" style="font-variation-settings:'SHRP' 70">Manyangles</text></g>` : ""}
|
||||||
|
</g>
|
||||||
|
</svg>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function signPrintHtml(svg: string, paper: SignPaper | SignDesign) {
|
||||||
|
const size = signPaper(typeof paper === "string" ? { paper } : paper);
|
||||||
|
return `<!doctype html><html><head><title>Manyangles guest sign</title><style>@page{size:${size.width} ${size.height};margin:0}html,body{margin:0;padding:0;background:white}svg{display:block;width:${size.width};height:${size.height}}*{print-color-adjust:exact;-webkit-print-color-adjust:exact}</style></head><body>${svg}</body></html>`;
|
||||||
|
}
|
||||||
@@ -20,7 +20,7 @@ export const bannersRouter = createTRPCRouter({
|
|||||||
if (!limit.allowed) throw new TRPCError({ code: "TOO_MANY_REQUESTS", message: "Too many banner uploads. Try again shortly." });
|
if (!limit.allowed) throw new TRPCError({ code: "TOO_MANY_REQUESTS", message: "Too many banner uploads. Try again shortly." });
|
||||||
const id = randomUUID();
|
const id = randomUUID();
|
||||||
const originalKey = `events/${input.eventId}/banners/${id}/original`;
|
const originalKey = `events/${input.eventId}/banners/${id}/original`;
|
||||||
await getDb().insert(eventBanners).values({ id, eventId: input.eventId, originalKey, contentType: input.contentType, byteSize: input.byteSize });
|
await getDb().insert(eventBanners).values({ id, eventId: input.eventId, originalKey, contentType: input.contentType, byteSize: input.byteSize, crop: input.crop });
|
||||||
return { bannerId: id, uploadUrl: await createPresignedPutUrl({ key: originalKey, contentType: input.contentType }) };
|
return { bannerId: id, uploadUrl: await createPresignedPutUrl({ key: originalKey, contentType: input.contentType }) };
|
||||||
}),
|
}),
|
||||||
complete: protectedProcedure.input(bannerInputSchema).mutation(async ({ ctx, input }) => {
|
complete: protectedProcedure.input(bannerInputSchema).mutation(async ({ ctx, input }) => {
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import QRCode from "qrcode";
|
||||||
|
|
||||||
|
export function signQr(url: string) {
|
||||||
|
const parsed = new URL(url);
|
||||||
|
if (!["http:", "https:"].includes(parsed.protocol)) throw new Error("Guest link must be HTTP or HTTPS");
|
||||||
|
const { modules } = QRCode.create(url, { errorCorrectionLevel: "M" });
|
||||||
|
let path = "";
|
||||||
|
for (let y = 0; y < modules.size; y++) {
|
||||||
|
for (let x = 0; x < modules.size; x++) {
|
||||||
|
if (modules.get(y, x)) path += `M${x} ${y}h1v1h-1z`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { path, size: modules.size };
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
import { bannerExtract } from "./banner-crop";
|
||||||
|
import { bannerCropSchema } from "@album/contracts";
|
||||||
|
|
||||||
|
describe("banner crop", () => {
|
||||||
|
test("centers a legacy portrait in exact 8:3", () => {
|
||||||
|
expect(bannerExtract(1200, 2400)).toEqual({ left: 0, top: 975, width: 1200, height: 450 });
|
||||||
|
});
|
||||||
|
test("honors an off-center crop", () => {
|
||||||
|
expect(bannerExtract(4000, 3000, { x: 10, y: 20, width: 80, height: 40 })).toEqual({ left: 400, top: 600, width: 3200, height: 1200 });
|
||||||
|
});
|
||||||
|
test("keeps rounding inside original bounds", () => {
|
||||||
|
const crop = bannerExtract(1001, 751, { x: 0, y: 0, width: 100, height: 100 });
|
||||||
|
expect(crop.width / crop.height).toBe(8 / 3);
|
||||||
|
expect(crop.left + crop.width).toBeLessThanOrEqual(1001);
|
||||||
|
expect(crop.top + crop.height).toBeLessThanOrEqual(751);
|
||||||
|
});
|
||||||
|
test("rejects invalid and empty crops", () => {
|
||||||
|
expect(bannerCropSchema.safeParse({ x: 95, y: 0, width: 10, height: 20 }).success).toBe(false);
|
||||||
|
expect(() => bannerExtract(1000, 1000, { x: 0, y: 0, width: 0.01, height: 0.01 })).toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { bannerCropSchema, type BannerCrop } from "@album/contracts";
|
||||||
|
|
||||||
|
// Percent coordinates refer to the auto-oriented original, never the thumbnail.
|
||||||
|
export function bannerExtract(width: number, height: number, input?: BannerCrop | null) {
|
||||||
|
const crop = input ? bannerCropSchema.parse(input) : { x: 0, y: 0, width: 100, height: 100 };
|
||||||
|
const availableWidth = Math.min(width, width * crop.width / 100);
|
||||||
|
const availableHeight = Math.min(height, height * crop.height / 100);
|
||||||
|
// Whole multiples guarantee an exact 8:3 output, including small images.
|
||||||
|
const unit = Math.floor(Math.min(availableWidth / 8, availableHeight / 3));
|
||||||
|
if (unit < 1) throw new Error("Banner crop is too small");
|
||||||
|
const croppedWidth = unit * 8;
|
||||||
|
const croppedHeight = unit * 3;
|
||||||
|
return {
|
||||||
|
left: Math.max(0, Math.min(width - croppedWidth, Math.round(width * crop.x / 100 + (availableWidth - croppedWidth) / 2))),
|
||||||
|
top: Math.max(0, Math.min(height - croppedHeight, Math.round(height * crop.y / 100 + (availableHeight - croppedHeight) / 2))),
|
||||||
|
width: croppedWidth,
|
||||||
|
height: croppedHeight,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { expect, test } from "bun:test";
|
||||||
|
import sharp from "sharp";
|
||||||
|
import { bannerVariant, photoVariants, VARIANT_CONTENT_TYPE } from "./image-variants";
|
||||||
|
|
||||||
|
test("photo variants are bounded WebP files and leave original bytes untouched", async () => {
|
||||||
|
const original = await sharp({ create: { width: 3000, height: 1500, channels: 3, background: "#4878ab" } }).jpeg().toBuffer();
|
||||||
|
const before = Buffer.from(original);
|
||||||
|
const [display, thumb] = await photoVariants(sharp(original).rotate());
|
||||||
|
expect(VARIANT_CONTENT_TYPE).toBe("image/webp");
|
||||||
|
for (const [bytes, width, height] of [[display, 2048, 1024], [thumb, 400, 200]] as const) {
|
||||||
|
const metadata = await sharp(bytes).metadata();
|
||||||
|
expect(metadata.format).toBe("webp");
|
||||||
|
expect(metadata.width).toBe(width);
|
||||||
|
expect(metadata.height).toBe(height);
|
||||||
|
}
|
||||||
|
expect(original.equals(before)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("small images are not enlarged and transparency survives", async () => {
|
||||||
|
const image = sharp({ create: { width: 80, height: 120, channels: 4, background: { r: 20, g: 40, b: 60, alpha: 0.5 } } });
|
||||||
|
for (const bytes of await photoVariants(image)) {
|
||||||
|
const metadata = await sharp(bytes).metadata();
|
||||||
|
expect(metadata.width).toBe(80);
|
||||||
|
expect(metadata.height).toBe(120);
|
||||||
|
expect(metadata.hasAlpha).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("banner generates an exact 8:3 WebP crop", async () => {
|
||||||
|
const bytes = await bannerVariant(sharp({ create: { width: 3200, height: 2400, channels: 3, background: "#4878ab" } }), { x: 0, y: 25, width: 100, height: 50 });
|
||||||
|
const metadata = await sharp(bytes).metadata();
|
||||||
|
expect(metadata.format).toBe("webp");
|
||||||
|
expect(metadata.width).toBe(2400);
|
||||||
|
expect(metadata.height).toBe(900);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("auto-orientation is applied before sizing WebP variants", async () => {
|
||||||
|
const source = await sharp({ create: { width: 120, height: 80, channels: 3, background: "#4878ab" } }).jpeg().withMetadata({ orientation: 6 }).toBuffer();
|
||||||
|
const [display] = await photoVariants(sharp(source).rotate());
|
||||||
|
const metadata = await sharp(display).metadata();
|
||||||
|
expect(metadata.width).toBe(80);
|
||||||
|
expect(metadata.height).toBe(120);
|
||||||
|
expect(metadata.orientation).toBeUndefined();
|
||||||
|
});
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import sharp, { type Sharp } from "sharp";
|
||||||
|
import type { BannerCrop } from "@album/contracts";
|
||||||
|
import { bannerExtract } from "./banner-crop";
|
||||||
|
|
||||||
|
export const VARIANT_CONTENT_TYPE = "image/webp";
|
||||||
|
|
||||||
|
// Callers supply an auto-oriented image. Never modify the stored original.
|
||||||
|
export function photoVariants(image: Sharp) {
|
||||||
|
return Promise.all([
|
||||||
|
image.clone().resize({ width: 2048, height: 2048, fit: "inside", withoutEnlargement: true })
|
||||||
|
.webp({ quality: 82, effort: 4 }).toBuffer(),
|
||||||
|
image.clone().resize({ width: 400, height: 400, fit: "inside", withoutEnlargement: true })
|
||||||
|
.webp({ quality: 75, effort: 4 }).toBuffer(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function bannerVariant(image: Sharp, crop?: BannerCrop | null) {
|
||||||
|
const { data, info } = await image.raw().toBuffer({ resolveWithObject: true });
|
||||||
|
return sharp(data, { raw: info }).extract(bannerExtract(info.width, info.height, crop))
|
||||||
|
.resize({ width: 2400, height: 900, fit: "inside", withoutEnlargement: true })
|
||||||
|
.webp({ quality: 82, effort: 4 }).toBuffer();
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import { sql } from "drizzle-orm";
|
import { sql } from "drizzle-orm";
|
||||||
import { and, eq } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
import sharp from "sharp";
|
import sharp from "sharp";
|
||||||
|
import { bannerVariant, photoVariants, VARIANT_CONTENT_TYPE } from "./image-variants";
|
||||||
|
import type { BannerCrop } from "@album/contracts";
|
||||||
import { processPhotoExport } from "./exports";
|
import { processPhotoExport } from "./exports";
|
||||||
import { processEmailDelivery } from "@album/email/queue";
|
import { processEmailDelivery } from "@album/email/queue";
|
||||||
import convert from "heic-convert";
|
import convert from "heic-convert";
|
||||||
@@ -84,31 +86,10 @@ async function processJob(job: ClaimedJob) {
|
|||||||
const metadata = await image.metadata();
|
const metadata = await image.metadata();
|
||||||
const displayKey = displayObjectKey(photo.eventId, photo.id);
|
const displayKey = displayObjectKey(photo.eventId, photo.id);
|
||||||
const thumbKey = thumbObjectKey(photo.eventId, photo.id);
|
const thumbKey = thumbObjectKey(photo.eventId, photo.id);
|
||||||
const [display, thumb] = await Promise.all([
|
const [display, thumb] = await photoVariants(image);
|
||||||
image
|
|
||||||
.clone()
|
|
||||||
.resize({
|
|
||||||
width: 2048,
|
|
||||||
height: 2048,
|
|
||||||
fit: "inside",
|
|
||||||
withoutEnlargement: true,
|
|
||||||
})
|
|
||||||
.jpeg({ quality: 82, mozjpeg: true })
|
|
||||||
.toBuffer(),
|
|
||||||
image
|
|
||||||
.clone()
|
|
||||||
.resize({
|
|
||||||
width: 400,
|
|
||||||
height: 400,
|
|
||||||
fit: "inside",
|
|
||||||
withoutEnlargement: true,
|
|
||||||
})
|
|
||||||
.jpeg({ quality: 75, mozjpeg: true })
|
|
||||||
.toBuffer(),
|
|
||||||
]);
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
putObject({ key: displayKey, body: display, contentType: "image/jpeg" }),
|
putObject({ key: displayKey, body: display, contentType: VARIANT_CONTENT_TYPE }),
|
||||||
putObject({ key: thumbKey, body: thumb, contentType: "image/jpeg" }),
|
putObject({ key: thumbKey, body: thumb, contentType: VARIANT_CONTENT_TYPE }),
|
||||||
]);
|
]);
|
||||||
await db.transaction(async (tx) => {
|
await db.transaction(async (tx) => {
|
||||||
await tx
|
await tx
|
||||||
@@ -176,18 +157,17 @@ async function processBanner() {
|
|||||||
SELECT id FROM event_banners
|
SELECT id FROM event_banners
|
||||||
WHERE status = 'pending' OR (status = 'processing' AND updated_at < now() - interval '10 minutes')
|
WHERE status = 'pending' OR (status = 'processing' AND updated_at < now() - interval '10 minutes')
|
||||||
ORDER BY created_at FOR UPDATE SKIP LOCKED LIMIT 1
|
ORDER BY created_at FOR UPDATE SKIP LOCKED LIMIT 1
|
||||||
) RETURNING id, event_id, original_key, content_type
|
) RETURNING id, event_id, original_key, content_type, crop
|
||||||
`);
|
`);
|
||||||
const banner = (rows as unknown as { id: string; event_id: string; original_key: string; content_type: string }[])[0];
|
const banner = (rows as unknown as { id: string; event_id: string; original_key: string; content_type: string; crop: BannerCrop | null }[])[0];
|
||||||
if (!banner) return false;
|
if (!banner) return false;
|
||||||
const scope = and(eq(eventBanners.id, banner.id), eq(eventBanners.eventId, banner.event_id));
|
const scope = and(eq(eventBanners.id, banner.id), eq(eventBanners.eventId, banner.event_id));
|
||||||
try {
|
try {
|
||||||
const original = await getObjectBuffer(banner.original_key);
|
const original = await getObjectBuffer(banner.original_key);
|
||||||
const image = await decodeImage(original, banner.content_type);
|
const image = await decodeImage(original, banner.content_type);
|
||||||
const display = await image.resize({ width: 2400, height: 2400, fit: "inside", withoutEnlargement: true })
|
const display = await bannerVariant(image, banner.crop);
|
||||||
.jpeg({ quality: 82, mozjpeg: true }).toBuffer();
|
const displayKey = `events/${banner.event_id}/banners/${banner.id}/display.webp`;
|
||||||
const displayKey = `events/${banner.event_id}/banners/${banner.id}/display.jpg`;
|
await putObject({ key: displayKey, body: display, contentType: VARIANT_CONTENT_TYPE });
|
||||||
await putObject({ key: displayKey, body: display, contentType: "image/jpeg" });
|
|
||||||
await getDb().update(eventBanners).set({ status: "ready", displayKey, updatedAt: new Date() }).where(scope);
|
await getDb().update(eventBanners).set({ status: "ready", displayKey, updatedAt: new Date() }).where(scope);
|
||||||
console.info(`Banner processed for event ${banner.event_id}`);
|
console.info(`Banner processed for event ${banner.event_id}`);
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -18,6 +18,9 @@
|
|||||||
"@album/database": "workspace:*",
|
"@album/database": "workspace:*",
|
||||||
"@album/email": "workspace:*",
|
"@album/email": "workspace:*",
|
||||||
"@album/storage": "workspace:*",
|
"@album/storage": "workspace:*",
|
||||||
|
"@fontsource-variable/funnel-display": "^5.3.0",
|
||||||
|
"@fontsource-variable/geologica": "^5.3.0",
|
||||||
|
"@fontsource-variable/inter": "^5.3.0",
|
||||||
"@radix-ui/react-popover": "^1.1.23",
|
"@radix-ui/react-popover": "^1.1.23",
|
||||||
"@tanstack/react-query": "^5.90.2",
|
"@tanstack/react-query": "^5.90.2",
|
||||||
"@trpc/client": "^11.4.3",
|
"@trpc/client": "^11.4.3",
|
||||||
@@ -30,13 +33,16 @@
|
|||||||
"date-fns": "^4.4.0",
|
"date-fns": "^4.4.0",
|
||||||
"dotenv": "^16.5.0",
|
"dotenv": "^16.5.0",
|
||||||
"drizzle-orm": "^0.45.2",
|
"drizzle-orm": "^0.45.2",
|
||||||
|
"heic-to": "^1.5.2",
|
||||||
"lucide-react": "^0.468.0",
|
"lucide-react": "^0.468.0",
|
||||||
"next": "16.2.12",
|
"next": "16.2.12",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
|
"qrcode": "^1.5.4",
|
||||||
"radix-ui": "^1.6.7",
|
"radix-ui": "^1.6.7",
|
||||||
"react": "19.2.8",
|
"react": "19.2.8",
|
||||||
"react-day-picker": "^9",
|
"react-day-picker": "^9",
|
||||||
"react-dom": "19.2.8",
|
"react-dom": "19.2.8",
|
||||||
|
"react-image-crop": "^11.1.2",
|
||||||
"server-only": "^0.0.1",
|
"server-only": "^0.0.1",
|
||||||
"shadcn": "^4.21.0",
|
"shadcn": "^4.21.0",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
@@ -49,9 +55,12 @@
|
|||||||
"@tailwindcss/postcss": "^4.1.10",
|
"@tailwindcss/postcss": "^4.1.10",
|
||||||
"@types/bun": "^1.3.14",
|
"@types/bun": "^1.3.14",
|
||||||
"@types/node": "^22.15.32",
|
"@types/node": "^22.15.32",
|
||||||
|
"@types/qrcode": "^1.5.6",
|
||||||
"@types/react": "19.2.17",
|
"@types/react": "19.2.17",
|
||||||
"@types/react-dom": "19.2.3",
|
"@types/react-dom": "19.2.3",
|
||||||
|
"jsqr": "^1.4.0",
|
||||||
"postcss": "^8.5.22",
|
"postcss": "^8.5.22",
|
||||||
|
"sharp": "0.35.3",
|
||||||
"tailwindcss": "^4.1.10",
|
"tailwindcss": "^4.1.10",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
},
|
},
|
||||||
@@ -330,6 +339,12 @@
|
|||||||
|
|
||||||
"@floating-ui/utils": ["@floating-ui/utils@0.2.12", "", {}, "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww=="],
|
"@floating-ui/utils": ["@floating-ui/utils@0.2.12", "", {}, "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww=="],
|
||||||
|
|
||||||
|
"@fontsource-variable/funnel-display": ["@fontsource-variable/funnel-display@5.3.0", "", {}, "sha512-o/12lyrfflx5QAQFa+hAl0uBeWoBUAm5w71j+KssBseo++XJT3tnveDSg5IAnAdiclQHfFuEZiL5Wn2BjuFxtw=="],
|
||||||
|
|
||||||
|
"@fontsource-variable/geologica": ["@fontsource-variable/geologica@5.3.0", "", {}, "sha512-N4pGIVYOXkyXDMooL/HeNYWbx7aMxRP+xABgkjZwi6bVeM/GM/cqHdO8UNFQiR5QR52yLI1RrzgKxm0DKWO2wQ=="],
|
||||||
|
|
||||||
|
"@fontsource-variable/inter": ["@fontsource-variable/inter@5.3.0", "", {}, "sha512-OupL48va4JNofb97w6NYeF9S7W/kHNKM0Er8Dem5nqi4jeOLrVJDoE8tZEpnMJmtkvNbB1EIPPwHcdkF6b1oUA=="],
|
||||||
|
|
||||||
"@hono/node-server": ["@hono/node-server@2.1.1", "", { "peerDependencies": { "hono": "^4" } }, "sha512-ELuehkj5VCBdgEw9zs+ivkKwyzzUCSQuE96YmiPvn1ECBoZCczbFXJLeEGMTYjphP6gydh4pHMqEYPVMYUVgQg=="],
|
"@hono/node-server": ["@hono/node-server@2.1.1", "", { "peerDependencies": { "hono": "^4" } }, "sha512-ELuehkj5VCBdgEw9zs+ivkKwyzzUCSQuE96YmiPvn1ECBoZCczbFXJLeEGMTYjphP6gydh4pHMqEYPVMYUVgQg=="],
|
||||||
|
|
||||||
"@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="],
|
"@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="],
|
||||||
@@ -376,7 +391,7 @@
|
|||||||
|
|
||||||
"@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.3.2" }, "os": "linux", "cpu": "x64" }, "sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg=="],
|
"@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.3.2" }, "os": "linux", "cpu": "x64" }, "sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg=="],
|
||||||
|
|
||||||
"@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="],
|
"@img/sharp-wasm32": ["@img/sharp-wasm32@0.35.3", "", { "dependencies": { "@emnapi/runtime": "^1.11.1" } }, "sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w=="],
|
||||||
|
|
||||||
"@img/sharp-webcontainers-wasm32": ["@img/sharp-webcontainers-wasm32@0.35.3", "", { "dependencies": { "@img/sharp-wasm32": "0.35.3" }, "cpu": "none" }, "sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q=="],
|
"@img/sharp-webcontainers-wasm32": ["@img/sharp-webcontainers-wasm32@0.35.3", "", { "dependencies": { "@img/sharp-wasm32": "0.35.3" }, "cpu": "none" }, "sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q=="],
|
||||||
|
|
||||||
@@ -634,6 +649,8 @@
|
|||||||
|
|
||||||
"@types/nodemailer": ["@types/nodemailer@8.0.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-PxpaInm8V1JQDd4j0ds5HfvWQk8JupS1C0Picb96QJsrrRDjBH+DlK7L4ZdNSqNULhiZRQHc40nLVShaGxXAMw=="],
|
"@types/nodemailer": ["@types/nodemailer@8.0.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-PxpaInm8V1JQDd4j0ds5HfvWQk8JupS1C0Picb96QJsrrRDjBH+DlK7L4ZdNSqNULhiZRQHc40nLVShaGxXAMw=="],
|
||||||
|
|
||||||
|
"@types/qrcode": ["@types/qrcode@1.5.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw=="],
|
||||||
|
|
||||||
"@types/react": ["@types/react@19.2.17", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw=="],
|
"@types/react": ["@types/react@19.2.17", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw=="],
|
||||||
|
|
||||||
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
|
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
|
||||||
@@ -654,6 +671,8 @@
|
|||||||
|
|
||||||
"ansi-regex": ["ansi-regex@6.3.0", "", {}, "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ=="],
|
"ansi-regex": ["ansi-regex@6.3.0", "", {}, "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ=="],
|
||||||
|
|
||||||
|
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||||
|
|
||||||
"archiver": ["archiver@8.0.0", "", { "dependencies": { "async": "^3.2.4", "buffer-crc32": "^1.0.0", "is-stream": "^4.0.0", "lazystream": "^1.0.0", "normalize-path": "^3.0.0", "readable-stream": "^4.0.0", "readdir-glob": "^3.0.0", "tar-stream": "^3.0.0", "zip-stream": "^7.0.2" } }, "sha512-fV1orZfsnPn9BaSByR/qE67rJCLJEy2Ox5bq7nJh+jquWaNh6Sfec75kJ2T6PtdGUbPQlrVoSVCEOa5SdiTQ1g=="],
|
"archiver": ["archiver@8.0.0", "", { "dependencies": { "async": "^3.2.4", "buffer-crc32": "^1.0.0", "is-stream": "^4.0.0", "lazystream": "^1.0.0", "normalize-path": "^3.0.0", "readable-stream": "^4.0.0", "readdir-glob": "^3.0.0", "tar-stream": "^3.0.0", "zip-stream": "^7.0.2" } }, "sha512-fV1orZfsnPn9BaSByR/qE67rJCLJEy2Ox5bq7nJh+jquWaNh6Sfec75kJ2T6PtdGUbPQlrVoSVCEOa5SdiTQ1g=="],
|
||||||
|
|
||||||
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
||||||
@@ -716,6 +735,8 @@
|
|||||||
|
|
||||||
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
|
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
|
||||||
|
|
||||||
|
"camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="],
|
||||||
|
|
||||||
"caniuse-lite": ["caniuse-lite@1.0.30001810", "", {}, "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg=="],
|
"caniuse-lite": ["caniuse-lite@1.0.30001810", "", {}, "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg=="],
|
||||||
|
|
||||||
"chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
|
"chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
|
||||||
@@ -728,12 +749,18 @@
|
|||||||
|
|
||||||
"client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="],
|
"client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="],
|
||||||
|
|
||||||
|
"cliui": ["cliui@6.0.0", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="],
|
||||||
|
|
||||||
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||||
|
|
||||||
"cn": ["cn@0.2.6", "", { "bin": { "cn": "bin/cn.mjs" } }, "sha512-+i4L0zUGgRcEnhsxueVrP7iBGxBx5iD0WOTYg1MwFEu2ZyCmH5Ov2V1cul2Ht5UchRQQgftCf4be/RxspuW6QQ=="],
|
"cn": ["cn@0.2.6", "", { "bin": { "cn": "bin/cn.mjs" } }, "sha512-+i4L0zUGgRcEnhsxueVrP7iBGxBx5iD0WOTYg1MwFEu2ZyCmH5Ov2V1cul2Ht5UchRQQgftCf4be/RxspuW6QQ=="],
|
||||||
|
|
||||||
"code-block-writer": ["code-block-writer@13.0.3", "", {}, "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg=="],
|
"code-block-writer": ["code-block-writer@13.0.3", "", {}, "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg=="],
|
||||||
|
|
||||||
|
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||||
|
|
||||||
|
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
||||||
|
|
||||||
"commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="],
|
"commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="],
|
||||||
|
|
||||||
"compress-commons": ["compress-commons@7.0.1", "", { "dependencies": { "crc-32": "^1.2.0", "crc32-stream": "^7.0.1", "is-stream": "^4.0.0", "normalize-path": "^3.0.0", "readable-stream": "^4.0.0" } }, "sha512-g0S8KAD8qf4+V//pr3BfB1aBnARLXNz2Gx+jmHU0LEriUuoQUOPOulVquHKTJ8+EAIIO7fhseNDr9wK5Q9FKBQ=="],
|
"compress-commons": ["compress-commons@7.0.1", "", { "dependencies": { "crc-32": "^1.2.0", "crc32-stream": "^7.0.1", "is-stream": "^4.0.0", "normalize-path": "^3.0.0", "readable-stream": "^4.0.0" } }, "sha512-g0S8KAD8qf4+V//pr3BfB1aBnARLXNz2Gx+jmHU0LEriUuoQUOPOulVquHKTJ8+EAIIO7fhseNDr9wK5Q9FKBQ=="],
|
||||||
@@ -776,6 +803,8 @@
|
|||||||
|
|
||||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||||
|
|
||||||
|
"decamelize": ["decamelize@1.2.0", "", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="],
|
||||||
|
|
||||||
"dedent": ["dedent@1.7.2", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA=="],
|
"dedent": ["dedent@1.7.2", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA=="],
|
||||||
|
|
||||||
"deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="],
|
"deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="],
|
||||||
@@ -796,6 +825,8 @@
|
|||||||
|
|
||||||
"diff": ["diff@8.0.4", "", {}, "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw=="],
|
"diff": ["diff@8.0.4", "", {}, "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw=="],
|
||||||
|
|
||||||
|
"dijkstrajs": ["dijkstrajs@1.0.3", "", {}, "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA=="],
|
||||||
|
|
||||||
"dot-prop": ["dot-prop@6.0.1", "", { "dependencies": { "is-obj": "^2.0.0" } }, "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA=="],
|
"dot-prop": ["dot-prop@6.0.1", "", { "dependencies": { "is-obj": "^2.0.0" } }, "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA=="],
|
||||||
|
|
||||||
"dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
|
"dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
|
||||||
@@ -810,7 +841,7 @@
|
|||||||
|
|
||||||
"electron-to-chromium": ["electron-to-chromium@1.5.422", "", {}, "sha512-UvA/32XqrLDdZSn7Jllo1AYNcWji/G0d5M0GTViE7KoGBiMunw3a34Sb2KO4ZZyrSEhqsxFoVhWWJshdyfKqJA=="],
|
"electron-to-chromium": ["electron-to-chromium@1.5.422", "", {}, "sha512-UvA/32XqrLDdZSn7Jllo1AYNcWji/G0d5M0GTViE7KoGBiMunw3a34Sb2KO4ZZyrSEhqsxFoVhWWJshdyfKqJA=="],
|
||||||
|
|
||||||
"emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
|
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||||
|
|
||||||
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
|
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
|
||||||
|
|
||||||
@@ -876,7 +907,7 @@
|
|||||||
|
|
||||||
"finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="],
|
"finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="],
|
||||||
|
|
||||||
"find-up": ["find-up@3.0.0", "", { "dependencies": { "locate-path": "^3.0.0" } }, "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg=="],
|
"find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="],
|
||||||
|
|
||||||
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
|
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
|
||||||
|
|
||||||
@@ -892,6 +923,8 @@
|
|||||||
|
|
||||||
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
|
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
|
||||||
|
|
||||||
|
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
|
||||||
|
|
||||||
"get-east-asian-width": ["get-east-asian-width@1.6.0", "", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="],
|
"get-east-asian-width": ["get-east-asian-width@1.6.0", "", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="],
|
||||||
|
|
||||||
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
||||||
@@ -920,6 +953,8 @@
|
|||||||
|
|
||||||
"heic-decode": ["heic-decode@2.1.0", "", { "dependencies": { "libheif-js": "^1.19.8" } }, "sha512-0fB3O3WMk38+PScbHLVp66jcNhsZ/ErtQ6u2lMYu/YxXgbBtl+oKOhGQHa4RpvE68k8IzbWkABzHnyAIjR758A=="],
|
"heic-decode": ["heic-decode@2.1.0", "", { "dependencies": { "libheif-js": "^1.19.8" } }, "sha512-0fB3O3WMk38+PScbHLVp66jcNhsZ/ErtQ6u2lMYu/YxXgbBtl+oKOhGQHa4RpvE68k8IzbWkABzHnyAIjR758A=="],
|
||||||
|
|
||||||
|
"heic-to": ["heic-to@1.5.2", "", {}, "sha512-8Fns+lZHAWmz5U5IUxDeXKwIf3foBoKNPLxxFY4B0MkLjNuomEIHCoDbDE+x/llFK3NCEO1cu4+n3iUKY+Svmw=="],
|
||||||
|
|
||||||
"hono": ["hono@4.13.7", "", {}, "sha512-c8/gF9ac8Y78/agExVocyLevgR+JlpNB444Py0FSX8pJoPdYUfUzRcXtYEYGwt6l19qIlVZPN5Mfsw9jFShmQQ=="],
|
"hono": ["hono@4.13.7", "", {}, "sha512-c8/gF9ac8Y78/agExVocyLevgR+JlpNB444Py0FSX8pJoPdYUfUzRcXtYEYGwt6l19qIlVZPN5Mfsw9jFShmQQ=="],
|
||||||
|
|
||||||
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
|
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
|
||||||
@@ -946,6 +981,8 @@
|
|||||||
|
|
||||||
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
|
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
|
||||||
|
|
||||||
|
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
||||||
|
|
||||||
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
|
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
|
||||||
|
|
||||||
"is-in-ssh": ["is-in-ssh@1.0.0", "", {}, "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw=="],
|
"is-in-ssh": ["is-in-ssh@1.0.0", "", {}, "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw=="],
|
||||||
@@ -996,6 +1033,8 @@
|
|||||||
|
|
||||||
"jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="],
|
"jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="],
|
||||||
|
|
||||||
|
"jsqr": ["jsqr@1.4.0", "", {}, "sha512-dxLob7q65Xg2DvstYkRpkYtmKm2sPJ9oFhrhmudT1dZvNFFTlroai3AWSpLey/w5vMcLBXRgOJsbXpdN9HzU/A=="],
|
||||||
|
|
||||||
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
|
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
|
||||||
|
|
||||||
"kysely": ["kysely@0.29.5", "", {}, "sha512-ooa+eSbBNPTo3MycPEuW5jdrxQdQwdtB3LC3h43FiXQbIry5tR0C5lDG7eealK0E4D7XjrnOP5DIUg/LyjRMYQ=="],
|
"kysely": ["kysely@0.29.5", "", {}, "sha512-ooa+eSbBNPTo3MycPEuW5jdrxQdQwdtB3LC3h43FiXQbIry5tR0C5lDG7eealK0E4D7XjrnOP5DIUg/LyjRMYQ=="],
|
||||||
@@ -1030,7 +1069,7 @@
|
|||||||
|
|
||||||
"lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
|
"lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
|
||||||
|
|
||||||
"locate-path": ["locate-path@3.0.0", "", { "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" } }, "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A=="],
|
"locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="],
|
||||||
|
|
||||||
"log-symbols": ["log-symbols@6.0.0", "", { "dependencies": { "chalk": "^5.3.0", "is-unicode-supported": "^1.3.0" } }, "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw=="],
|
"log-symbols": ["log-symbols@6.0.0", "", { "dependencies": { "chalk": "^5.3.0", "is-unicode-supported": "^1.3.0" } }, "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw=="],
|
||||||
|
|
||||||
@@ -1102,7 +1141,7 @@
|
|||||||
|
|
||||||
"p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
|
"p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
|
||||||
|
|
||||||
"p-locate": ["p-locate@3.0.0", "", { "dependencies": { "p-limit": "^2.0.0" } }, "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ=="],
|
"p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="],
|
||||||
|
|
||||||
"p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="],
|
"p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="],
|
||||||
|
|
||||||
@@ -1116,7 +1155,7 @@
|
|||||||
|
|
||||||
"path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="],
|
"path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="],
|
||||||
|
|
||||||
"path-exists": ["path-exists@3.0.0", "", {}, "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ=="],
|
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
|
||||||
|
|
||||||
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||||
|
|
||||||
@@ -1130,7 +1169,7 @@
|
|||||||
|
|
||||||
"pkg-up": ["pkg-up@3.1.0", "", { "dependencies": { "find-up": "^3.0.0" } }, "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA=="],
|
"pkg-up": ["pkg-up@3.1.0", "", { "dependencies": { "find-up": "^3.0.0" } }, "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA=="],
|
||||||
|
|
||||||
"pngjs": ["pngjs@6.0.0", "", {}, "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg=="],
|
"pngjs": ["pngjs@5.0.0", "", {}, "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw=="],
|
||||||
|
|
||||||
"postal-mime": ["postal-mime@2.7.5", "", {}, "sha512-GNEXKvWFQnbgO5NlrGzVa0FmWzBZ24PersAWErttSg1Hjpf0ATxTwS5DOMGaOpTG6bUh5cTr7xi0jAD942wCJA=="],
|
"postal-mime": ["postal-mime@2.7.5", "", {}, "sha512-GNEXKvWFQnbgO5NlrGzVa0FmWzBZ24PersAWErttSg1Hjpf0ATxTwS5DOMGaOpTG6bUh5cTr7xi0jAD942wCJA=="],
|
||||||
|
|
||||||
@@ -1152,6 +1191,8 @@
|
|||||||
|
|
||||||
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
|
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
|
||||||
|
|
||||||
|
"qrcode": ["qrcode@1.5.4", "", { "dependencies": { "dijkstrajs": "^1.0.1", "pngjs": "^5.0.0", "yargs": "^15.3.1" }, "bin": { "qrcode": "bin/qrcode" } }, "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg=="],
|
||||||
|
|
||||||
"qs": ["qs@6.16.0", "", { "dependencies": { "es-define-property": "^1.0.1", "side-channel": "^1.1.1" } }, "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA=="],
|
"qs": ["qs@6.16.0", "", { "dependencies": { "es-define-property": "^1.0.1", "side-channel": "^1.1.1" } }, "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA=="],
|
||||||
|
|
||||||
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
|
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
|
||||||
@@ -1168,6 +1209,8 @@
|
|||||||
|
|
||||||
"react-dom": ["react-dom@19.2.8", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.8" } }, "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ=="],
|
"react-dom": ["react-dom@19.2.8", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.8" } }, "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ=="],
|
||||||
|
|
||||||
|
"react-image-crop": ["react-image-crop@11.1.2", "", { "peerDependencies": { "react": ">=16.13.1" } }, "sha512-+0Pc2fxpwKL4u4oLmdKBw8XSwUceFbXbKEHvFOlsl/MGB1OVNic4uBlAPmEHGXYgoJIq+b63xHbc/aJMG0AVkA=="],
|
||||||
|
|
||||||
"react-remove-scroll": ["react-remove-scroll@2.7.2", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="],
|
"react-remove-scroll": ["react-remove-scroll@2.7.2", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="],
|
||||||
|
|
||||||
"react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],
|
"react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],
|
||||||
@@ -1180,8 +1223,12 @@
|
|||||||
|
|
||||||
"recast": ["recast@0.23.21", "", { "dependencies": { "ast-types": "^0.16.1", "esprima": "~4.0.0", "source-map": "~0.6.1", "tiny-invariant": "^1.3.3", "tslib": "^2.0.1" } }, "sha512-mFAyJq9vUbSTARLZUvAEf1z3YxlvAwswbmxMx2mPA/MSm4KmpwvwvhsH/NIrZhyOuwD60Lzyw2qh83uCbgTPYw=="],
|
"recast": ["recast@0.23.21", "", { "dependencies": { "ast-types": "^0.16.1", "esprima": "~4.0.0", "source-map": "~0.6.1", "tiny-invariant": "^1.3.3", "tslib": "^2.0.1" } }, "sha512-mFAyJq9vUbSTARLZUvAEf1z3YxlvAwswbmxMx2mPA/MSm4KmpwvwvhsH/NIrZhyOuwD60Lzyw2qh83uCbgTPYw=="],
|
||||||
|
|
||||||
|
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
|
||||||
|
|
||||||
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
|
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
|
||||||
|
|
||||||
|
"require-main-filename": ["require-main-filename@2.0.0", "", {}, "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="],
|
||||||
|
|
||||||
"resend": ["resend@6.26.0", "", { "dependencies": { "postal-mime": "2.7.5", "standardwebhooks": "1.0.0" }, "peerDependencies": { "@react-email/render": "*" }, "optionalPeers": ["@react-email/render"] }, "sha512-vkrULozV5nGF8o7tyun/t0+qFgtPpn+cyRhVqeorhOsPUNDiSc/9p/pn8AprdHqJpYaTvftpnQgWurW++FkCdw=="],
|
"resend": ["resend@6.26.0", "", { "dependencies": { "postal-mime": "2.7.5", "standardwebhooks": "1.0.0" }, "peerDependencies": { "@react-email/render": "*" }, "optionalPeers": ["@react-email/render"] }, "sha512-vkrULozV5nGF8o7tyun/t0+qFgtPpn+cyRhVqeorhOsPUNDiSc/9p/pn8AprdHqJpYaTvftpnQgWurW++FkCdw=="],
|
||||||
|
|
||||||
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
|
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
|
||||||
@@ -1214,6 +1261,8 @@
|
|||||||
|
|
||||||
"server-only": ["server-only@0.0.1", "", {}, "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA=="],
|
"server-only": ["server-only@0.0.1", "", {}, "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA=="],
|
||||||
|
|
||||||
|
"set-blocking": ["set-blocking@2.0.0", "", {}, "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="],
|
||||||
|
|
||||||
"set-cookie-parser": ["set-cookie-parser@3.1.2", "", {}, "sha512-5/r/lTwbJ3zQ+qwdUFZYeRNqda7P5HD8zQKqlSjdGt1/S0cjLAphHusj4Y58ahDtWn/g32xrIS58/ikOvwl0Lw=="],
|
"set-cookie-parser": ["set-cookie-parser@3.1.2", "", {}, "sha512-5/r/lTwbJ3zQ+qwdUFZYeRNqda7P5HD8zQKqlSjdGt1/S0cjLAphHusj4Y58ahDtWn/g32xrIS58/ikOvwl0Lw=="],
|
||||||
|
|
||||||
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
|
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
|
||||||
@@ -1260,7 +1309,7 @@
|
|||||||
|
|
||||||
"streamx": ["streamx@2.28.1", "", { "dependencies": { "events-universal": "^1.0.0", "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" } }, "sha512-zEzXb0s5Cds7tqMH6rhZ05lcJydCWiQPEwiNngVqzsxCc962vLY4Uw+mW7od8kDH258k2Uz/JrOkdIAAhSh9VA=="],
|
"streamx": ["streamx@2.28.1", "", { "dependencies": { "events-universal": "^1.0.0", "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" } }, "sha512-zEzXb0s5Cds7tqMH6rhZ05lcJydCWiQPEwiNngVqzsxCc962vLY4Uw+mW7od8kDH258k2Uz/JrOkdIAAhSh9VA=="],
|
||||||
|
|
||||||
"string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
|
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||||
|
|
||||||
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
|
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
|
||||||
|
|
||||||
@@ -1336,12 +1385,22 @@
|
|||||||
|
|
||||||
"which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="],
|
"which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="],
|
||||||
|
|
||||||
|
"which-module": ["which-module@2.0.1", "", {}, "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ=="],
|
||||||
|
|
||||||
|
"wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="],
|
||||||
|
|
||||||
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
||||||
|
|
||||||
"wsl-utils": ["wsl-utils@1.0.0", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-Hl0ZOAs672vg+06kfujwRhoS6/jehvULrlFkuF2dRu6pHgA8U06h3xqNIqNNU1LTXPcedxByAR4GS6pwQK0mgA=="],
|
"wsl-utils": ["wsl-utils@1.0.0", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-Hl0ZOAs672vg+06kfujwRhoS6/jehvULrlFkuF2dRu6pHgA8U06h3xqNIqNNU1LTXPcedxByAR4GS6pwQK0mgA=="],
|
||||||
|
|
||||||
|
"y18n": ["y18n@4.0.3", "", {}, "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="],
|
||||||
|
|
||||||
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
||||||
|
|
||||||
|
"yargs": ["yargs@15.4.1", "", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } }, "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="],
|
||||||
|
|
||||||
|
"yargs-parser": ["yargs-parser@18.1.3", "", { "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } }, "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ=="],
|
||||||
|
|
||||||
"yocto-spinner": ["yocto-spinner@1.2.2", "", { "dependencies": { "yoctocolors": "^2.1.1" } }, "sha512-DODGl1wJjA/s5pnJFKau9lIYHT81lnhob1i3e1TjxZRxEhWRKl74nTbWE6H5KlkViQQTo/Z29YFdxzTZAMY3ng=="],
|
"yocto-spinner": ["yocto-spinner@1.2.2", "", { "dependencies": { "yoctocolors": "^2.1.1" } }, "sha512-DODGl1wJjA/s5pnJFKau9lIYHT81lnhob1i3e1TjxZRxEhWRKl74nTbWE6H5KlkViQQTo/Z29YFdxzTZAMY3ng=="],
|
||||||
|
|
||||||
"yoctocolors": ["yoctocolors@2.2.0", "", {}, "sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg=="],
|
"yoctocolors": ["yoctocolors@2.2.0", "", {}, "sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg=="],
|
||||||
@@ -1370,10 +1429,6 @@
|
|||||||
|
|
||||||
"@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="],
|
"@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="],
|
||||||
|
|
||||||
"@img/sharp-freebsd-wasm32/@img/sharp-wasm32": ["@img/sharp-wasm32@0.35.3", "", { "dependencies": { "@emnapi/runtime": "^1.11.1" } }, "sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w=="],
|
|
||||||
|
|
||||||
"@img/sharp-webcontainers-wasm32/@img/sharp-wasm32": ["@img/sharp-wasm32@0.35.3", "", { "dependencies": { "@emnapi/runtime": "^1.11.1" } }, "sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w=="],
|
|
||||||
|
|
||||||
"@modelcontextprotocol/sdk/zod": ["zod@4.5.4", "", {}, "sha512-sC95tT5iHHH9gtpj6A81kh+NEaRAUFN+qlUPDUbRfOMvNf5QCBqsb3WgvnpVtK5Y+4UfA6KqufotuTvMGiTlsA=="],
|
"@modelcontextprotocol/sdk/zod": ["zod@4.5.4", "", {}, "sha512-sC95tT5iHHH9gtpj6A81kh+NEaRAUFN+qlUPDUbRfOMvNf5QCBqsb3WgvnpVtK5Y+4UfA6KqufotuTvMGiTlsA=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.3", "tslib": "^2.4.0" }, "bundled": true }, "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg=="],
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.3", "tslib": "^2.4.0" }, "bundled": true }, "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg=="],
|
||||||
@@ -1392,6 +1447,8 @@
|
|||||||
|
|
||||||
"@types/nodemailer/@types/node": ["@types/node@26.5.0", "", { "dependencies": { "undici-types": "~8.9.0" } }, "sha512-dVSGpriSoCgz8WnDNTuSSuSv1PC/ALXihO4ulRZt7Md8k9mlbdin3lGOcDE8SnWOgf513ByWlXd7BK4azmyg/A=="],
|
"@types/nodemailer/@types/node": ["@types/node@26.5.0", "", { "dependencies": { "undici-types": "~8.9.0" } }, "sha512-dVSGpriSoCgz8WnDNTuSSuSv1PC/ALXihO4ulRZt7Md8k9mlbdin3lGOcDE8SnWOgf513ByWlXd7BK4azmyg/A=="],
|
||||||
|
|
||||||
|
"@types/qrcode/@types/node": ["@types/node@26.5.0", "", { "dependencies": { "undici-types": "~8.9.0" } }, "sha512-dVSGpriSoCgz8WnDNTuSSuSv1PC/ALXihO4ulRZt7Md8k9mlbdin3lGOcDE8SnWOgf513ByWlXd7BK4azmyg/A=="],
|
||||||
|
|
||||||
"@types/readdir-glob/@types/node": ["@types/node@26.5.0", "", { "dependencies": { "undici-types": "~8.9.0" } }, "sha512-dVSGpriSoCgz8WnDNTuSSuSv1PC/ALXihO4ulRZt7Md8k9mlbdin3lGOcDE8SnWOgf513ByWlXd7BK4azmyg/A=="],
|
"@types/readdir-glob/@types/node": ["@types/node@26.5.0", "", { "dependencies": { "undici-types": "~8.9.0" } }, "sha512-dVSGpriSoCgz8WnDNTuSSuSv1PC/ALXihO4ulRZt7Md8k9mlbdin3lGOcDE8SnWOgf513ByWlXd7BK4azmyg/A=="],
|
||||||
|
|
||||||
"better-auth/zod": ["zod@4.5.4", "", {}, "sha512-sC95tT5iHHH9gtpj6A81kh+NEaRAUFN+qlUPDUbRfOMvNf5QCBqsb3WgvnpVtK5Y+4UfA6KqufotuTvMGiTlsA=="],
|
"better-auth/zod": ["zod@4.5.4", "", {}, "sha512-sC95tT5iHHH9gtpj6A81kh+NEaRAUFN+qlUPDUbRfOMvNf5QCBqsb3WgvnpVtK5Y+4UfA6KqufotuTvMGiTlsA=="],
|
||||||
@@ -1400,6 +1457,8 @@
|
|||||||
|
|
||||||
"bun-types/@types/node": ["@types/node@26.5.0", "", { "dependencies": { "undici-types": "~8.9.0" } }, "sha512-dVSGpriSoCgz8WnDNTuSSuSv1PC/ALXihO4ulRZt7Md8k9mlbdin3lGOcDE8SnWOgf513ByWlXd7BK4azmyg/A=="],
|
"bun-types/@types/node": ["@types/node@26.5.0", "", { "dependencies": { "undici-types": "~8.9.0" } }, "sha512-dVSGpriSoCgz8WnDNTuSSuSv1PC/ALXihO4ulRZt7Md8k9mlbdin3lGOcDE8SnWOgf513ByWlXd7BK4azmyg/A=="],
|
||||||
|
|
||||||
|
"cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||||
|
|
||||||
"conf/ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="],
|
"conf/ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="],
|
||||||
|
|
||||||
"conf/json-schema-typed": ["json-schema-typed@7.0.3", "", {}, "sha512-7DE8mpG+/fVw+dTpjbxnx47TaMnDfOI1jwft9g1VybltZCduyRQPJPvc+zzKY9WPHxhPWczyFuYa6I8Mw4iU5A=="],
|
"conf/json-schema-typed": ["json-schema-typed@7.0.3", "", {}, "sha512-7DE8mpG+/fVw+dTpjbxnx47TaMnDfOI1jwft9g1VybltZCduyRQPJPvc+zzKY9WPHxhPWczyFuYa6I8Mw4iU5A=="],
|
||||||
@@ -1410,6 +1469,8 @@
|
|||||||
|
|
||||||
"enquirer/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
"enquirer/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||||
|
|
||||||
|
"heic-convert/pngjs": ["pngjs@6.0.0", "", {}, "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg=="],
|
||||||
|
|
||||||
"is-inside-container/is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="],
|
"is-inside-container/is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="],
|
||||||
|
|
||||||
"lazystream/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
|
"lazystream/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
|
||||||
@@ -1428,6 +1489,10 @@
|
|||||||
|
|
||||||
"onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
|
"onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
|
||||||
|
|
||||||
|
"ora/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
|
||||||
|
|
||||||
|
"pkg-up/find-up": ["find-up@3.0.0", "", { "dependencies": { "locate-path": "^3.0.0" } }, "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg=="],
|
||||||
|
|
||||||
"prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="],
|
"prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="],
|
||||||
|
|
||||||
"readable-stream/buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="],
|
"readable-stream/buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="],
|
||||||
@@ -1436,12 +1501,16 @@
|
|||||||
|
|
||||||
"stream-browserify/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
|
"stream-browserify/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
|
||||||
|
|
||||||
|
"string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||||
|
|
||||||
"string_decoder/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
"string_decoder/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||||
|
|
||||||
"tsx/esbuild": ["esbuild@0.28.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.2", "@esbuild/android-arm": "0.28.2", "@esbuild/android-arm64": "0.28.2", "@esbuild/android-x64": "0.28.2", "@esbuild/darwin-arm64": "0.28.2", "@esbuild/darwin-x64": "0.28.2", "@esbuild/freebsd-arm64": "0.28.2", "@esbuild/freebsd-x64": "0.28.2", "@esbuild/linux-arm": "0.28.2", "@esbuild/linux-arm64": "0.28.2", "@esbuild/linux-ia32": "0.28.2", "@esbuild/linux-loong64": "0.28.2", "@esbuild/linux-mips64el": "0.28.2", "@esbuild/linux-ppc64": "0.28.2", "@esbuild/linux-riscv64": "0.28.2", "@esbuild/linux-s390x": "0.28.2", "@esbuild/linux-x64": "0.28.2", "@esbuild/netbsd-arm64": "0.28.2", "@esbuild/netbsd-x64": "0.28.2", "@esbuild/openbsd-arm64": "0.28.2", "@esbuild/openbsd-x64": "0.28.2", "@esbuild/openharmony-arm64": "0.28.2", "@esbuild/sunos-x64": "0.28.2", "@esbuild/win32-arm64": "0.28.2", "@esbuild/win32-ia32": "0.28.2", "@esbuild/win32-x64": "0.28.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA=="],
|
"tsx/esbuild": ["esbuild@0.28.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.2", "@esbuild/android-arm": "0.28.2", "@esbuild/android-arm64": "0.28.2", "@esbuild/android-x64": "0.28.2", "@esbuild/darwin-arm64": "0.28.2", "@esbuild/darwin-x64": "0.28.2", "@esbuild/freebsd-arm64": "0.28.2", "@esbuild/freebsd-x64": "0.28.2", "@esbuild/linux-arm": "0.28.2", "@esbuild/linux-arm64": "0.28.2", "@esbuild/linux-ia32": "0.28.2", "@esbuild/linux-loong64": "0.28.2", "@esbuild/linux-mips64el": "0.28.2", "@esbuild/linux-ppc64": "0.28.2", "@esbuild/linux-riscv64": "0.28.2", "@esbuild/linux-s390x": "0.28.2", "@esbuild/linux-x64": "0.28.2", "@esbuild/netbsd-arm64": "0.28.2", "@esbuild/netbsd-x64": "0.28.2", "@esbuild/openbsd-arm64": "0.28.2", "@esbuild/openbsd-x64": "0.28.2", "@esbuild/openharmony-arm64": "0.28.2", "@esbuild/sunos-x64": "0.28.2", "@esbuild/win32-arm64": "0.28.2", "@esbuild/win32-ia32": "0.28.2", "@esbuild/win32-x64": "0.28.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA=="],
|
||||||
|
|
||||||
"type-is/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="],
|
"type-is/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="],
|
||||||
|
|
||||||
|
"wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||||
|
|
||||||
"wsl-utils/is-wsl": ["is-wsl@3.1.1", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw=="],
|
"wsl-utils/is-wsl": ["is-wsl@3.1.1", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw=="],
|
||||||
|
|
||||||
"wsl-utils/powershell-utils": ["powershell-utils@0.1.0", "", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="],
|
"wsl-utils/powershell-utils": ["powershell-utils@0.1.0", "", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="],
|
||||||
@@ -1508,10 +1577,14 @@
|
|||||||
|
|
||||||
"@types/nodemailer/@types/node/undici-types": ["undici-types@8.9.0", "", {}, "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg=="],
|
"@types/nodemailer/@types/node/undici-types": ["undici-types@8.9.0", "", {}, "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg=="],
|
||||||
|
|
||||||
|
"@types/qrcode/@types/node/undici-types": ["undici-types@8.9.0", "", {}, "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg=="],
|
||||||
|
|
||||||
"@types/readdir-glob/@types/node/undici-types": ["undici-types@8.9.0", "", {}, "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg=="],
|
"@types/readdir-glob/@types/node/undici-types": ["undici-types@8.9.0", "", {}, "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg=="],
|
||||||
|
|
||||||
"bun-types/@types/node/undici-types": ["undici-types@8.9.0", "", {}, "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg=="],
|
"bun-types/@types/node/undici-types": ["undici-types@8.9.0", "", {}, "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg=="],
|
||||||
|
|
||||||
|
"cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||||
|
|
||||||
"cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
"cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||||
|
|
||||||
"enquirer/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
"enquirer/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||||
@@ -1558,12 +1631,20 @@
|
|||||||
|
|
||||||
"next/sharp/@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="],
|
"next/sharp/@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="],
|
||||||
|
|
||||||
|
"next/sharp/@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="],
|
||||||
|
|
||||||
"next/sharp/@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="],
|
"next/sharp/@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="],
|
||||||
|
|
||||||
"next/sharp/@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="],
|
"next/sharp/@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="],
|
||||||
|
|
||||||
"next/sharp/@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="],
|
"next/sharp/@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="],
|
||||||
|
|
||||||
|
"ora/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
|
||||||
|
|
||||||
|
"pkg-up/find-up/locate-path": ["locate-path@3.0.0", "", { "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" } }, "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A=="],
|
||||||
|
|
||||||
|
"string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||||
|
|
||||||
"tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ=="],
|
"tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ=="],
|
||||||
|
|
||||||
"tsx/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.28.2", "", { "os": "android", "cpu": "arm" }, "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg=="],
|
"tsx/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.28.2", "", { "os": "android", "cpu": "arm" }, "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg=="],
|
||||||
@@ -1615,5 +1696,11 @@
|
|||||||
"tsx/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA=="],
|
"tsx/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA=="],
|
||||||
|
|
||||||
"tsx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g=="],
|
"tsx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g=="],
|
||||||
|
|
||||||
|
"wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||||
|
|
||||||
|
"pkg-up/find-up/locate-path/p-locate": ["p-locate@3.0.0", "", { "dependencies": { "p-limit": "^2.0.0" } }, "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ=="],
|
||||||
|
|
||||||
|
"pkg-up/find-up/locate-path/path-exists": ["path-exists@3.0.0", "", {}, "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ=="],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -173,10 +173,20 @@ export const completePhotoInputSchema = z.object({
|
|||||||
photoId: z.string().uuid(),
|
photoId: z.string().uuid(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const BANNER_ASPECT_RATIO = 8 / 3;
|
||||||
|
export const bannerCropSchema = z.object({
|
||||||
|
x: z.number().finite().min(0).max(100),
|
||||||
|
y: z.number().finite().min(0).max(100),
|
||||||
|
width: z.number().finite().positive().max(100),
|
||||||
|
height: z.number().finite().positive().max(100),
|
||||||
|
}).refine((crop) => crop.x + crop.width <= 100.001 && crop.y + crop.height <= 100.001, "Crop must fit inside the image");
|
||||||
|
export type BannerCrop = z.infer<typeof bannerCropSchema>;
|
||||||
|
|
||||||
export const createBannerInputSchema = z.object({
|
export const createBannerInputSchema = z.object({
|
||||||
eventId: z.string().uuid(),
|
eventId: z.string().uuid(),
|
||||||
contentType: allowedImageTypeSchema,
|
contentType: allowedImageTypeSchema,
|
||||||
byteSize: z.number().int().positive().max(MAX_PHOTO_BYTES),
|
byteSize: z.number().int().positive().max(MAX_PHOTO_BYTES),
|
||||||
|
crop: bannerCropSchema.optional(),
|
||||||
});
|
});
|
||||||
export const bannerInputSchema = z.object({
|
export const bannerInputSchema = z.object({
|
||||||
eventId: z.string().uuid(),
|
eventId: z.string().uuid(),
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE event_banners ADD COLUMN crop jsonb;
|
||||||
@@ -71,6 +71,13 @@
|
|||||||
"when": 1788906100000,
|
"when": 1788906100000,
|
||||||
"tag": "0009_photo_exports",
|
"tag": "0009_photo_exports",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 10,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1788996000000,
|
||||||
|
"tag": "0010_banner_crop",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -216,6 +216,7 @@ export const eventBanners = pgTable("event_banners", {
|
|||||||
eventId: uuid("event_id").notNull().references(() => events.id, { onDelete: "cascade" }),
|
eventId: uuid("event_id").notNull().references(() => events.id, { onDelete: "cascade" }),
|
||||||
originalKey: text("original_key").notNull(),
|
originalKey: text("original_key").notNull(),
|
||||||
displayKey: text("display_key"),
|
displayKey: text("display_key"),
|
||||||
|
crop: jsonb("crop").$type<import("@album/contracts").BannerCrop>(),
|
||||||
contentType: text("content_type").notNull(),
|
contentType: text("content_type").notNull(),
|
||||||
byteSize: integer("byte_size").notNull(),
|
byteSize: integer("byte_size").notNull(),
|
||||||
status: text("status").notNull().default("uploading"),
|
status: text("status").notNull().default("uploading"),
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ describe("object keys", () => {
|
|||||||
test("keeps originals and variants under the same photo prefix", () => {
|
test("keeps originals and variants under the same photo prefix", () => {
|
||||||
const prefix = photoObjectPrefix(eventId, photoId);
|
const prefix = photoObjectPrefix(eventId, photoId);
|
||||||
expect(originalObjectKey(eventId, photoId).startsWith(prefix)).toBe(true);
|
expect(originalObjectKey(eventId, photoId).startsWith(prefix)).toBe(true);
|
||||||
expect(displayObjectKey(eventId, photoId)).toBe(`${prefix}display.jpg`);
|
expect(displayObjectKey(eventId, photoId)).toBe(`${prefix}display.webp`);
|
||||||
expect(thumbObjectKey(eventId, photoId)).toBe(`${prefix}thumb.jpg`);
|
expect(thumbObjectKey(eventId, photoId)).toBe(`${prefix}thumb.webp`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ export function originalObjectKey(eventId: string, photoId: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function displayObjectKey(eventId: string, photoId: string) {
|
export function displayObjectKey(eventId: string, photoId: string) {
|
||||||
return `events/${eventId}/photos/${photoId}/display.jpg`;
|
return `events/${eventId}/photos/${photoId}/display.webp`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function thumbObjectKey(eventId: string, photoId: string) {
|
export function thumbObjectKey(eventId: string, photoId: string) {
|
||||||
return `events/${eventId}/photos/${photoId}/thumb.jpg`;
|
return `events/${eventId}/photos/${photoId}/thumb.webp`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function photoObjectPrefix(eventId: string, photoId: string) {
|
export function photoObjectPrefix(eventId: string, photoId: string) {
|
||||||
|
|||||||