Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
52326657d3 | ||
|
|
e2d097f918 | ||
|
|
573689d6cb | ||
|
|
dc250c9ff5 | ||
|
|
41f4aef614 | ||
|
|
ac323b240d | ||
|
|
2a2d9f50f1 | ||
|
|
91acfff93e |
@@ -12,8 +12,6 @@ export default defineConfig({
|
|||||||
i18n: {
|
i18n: {
|
||||||
locales: ['ua', 'en'],
|
locales: ['ua', 'en'],
|
||||||
defaultLocale: 'ua',
|
defaultLocale: 'ua',
|
||||||
routing: {
|
routing: 'manual',
|
||||||
prefixDefaultLocale: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,15 +15,24 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/netlify": "^8.2.1",
|
"@astrojs/netlify": "^8.2.1",
|
||||||
"@astrojs/react": "^6.0.2",
|
"@astrojs/react": "^6.0.2",
|
||||||
|
"@mdxeditor/editor": "^4.3.2",
|
||||||
"@supabase/ssr": "^0.7.0",
|
"@supabase/ssr": "^0.7.0",
|
||||||
"@supabase/supabase-js": "^2.55.0",
|
"@supabase/supabase-js": "^2.55.0",
|
||||||
|
"@tailwindcss/postcss": "^4.3.3",
|
||||||
|
"@tailwindcss/typography": "^0.5.20",
|
||||||
|
"@types/markdown-it": "^14.2.0",
|
||||||
"@types/react": "^19.2.18",
|
"@types/react": "^19.2.18",
|
||||||
"@types/react-dom": "^19.2.4",
|
"@types/react-dom": "^19.2.4",
|
||||||
"astro": "^7.2.1",
|
"astro": "^7.2.1",
|
||||||
|
"autoprefixer": "^10.6.1",
|
||||||
|
"isomorphic-dompurify": "^4.5.0",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
|
"markdown-it": "^15.0.2",
|
||||||
|
"postcss": "^8.5.29",
|
||||||
"react": "^19.2.8",
|
"react": "^19.2.8",
|
||||||
"react-dom": "^19.2.8",
|
"react-dom": "^19.2.8",
|
||||||
"react-leaflet": "^5.0.0"
|
"react-leaflet": "^5.0.0",
|
||||||
|
"tailwindcss": "^4.3.3"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"sharp": "0.35.3"
|
"sharp": "0.35.3"
|
||||||
@@ -31,6 +40,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@astrojs/check": "^0.9.10",
|
"@astrojs/check": "^0.9.10",
|
||||||
"@types/leaflet": "^1.9.22",
|
"@types/leaflet": "^1.9.22",
|
||||||
|
"sass": "^1.102.0",
|
||||||
"typescript": "^6.0.3"
|
"typescript": "^6.0.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
'@tailwindcss/postcss': {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 284 B After Width: | Height: | Size: 913 B |
|
Before Width: | Height: | Size: 474 B After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 655 B After Width: | Height: | Size: 2.3 KiB |
@@ -1,9 +1,35 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
<style>
|
|
||||||
path { fill: #000; }
|
<svg
|
||||||
@media (prefers-color-scheme: dark) {
|
width="39.688015mm"
|
||||||
path { fill: #FFF; }
|
height="39.688244mm"
|
||||||
}
|
viewBox="0 0 39.688015 39.688244"
|
||||||
</style>
|
version="1.1"
|
||||||
</svg>
|
id="svg1"
|
||||||
|
xml:space="preserve"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||||
|
id="defs1"><linearGradient
|
||||||
|
id="swatch3962"><stop
|
||||||
|
style="stop-color:#5a6334;stop-opacity:1;"
|
||||||
|
offset="0"
|
||||||
|
id="stop3962" /></linearGradient></defs><g
|
||||||
|
id="layer1"
|
||||||
|
transform="translate(-83.079156,-128.58724)"><circle
|
||||||
|
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.430698;stroke-opacity:1"
|
||||||
|
id="path3963"
|
||||||
|
cx="102.92317"
|
||||||
|
cy="148.43124"
|
||||||
|
r="19.844" /><path
|
||||||
|
style="fill:#5a6334;fill-opacity:1"
|
||||||
|
d="m 101.2073,165.73096 c -4.504241,-0.76809 -7.189631,-2.03794 -10.011961,-4.73438 -14.79928,-14.13914 3.18934,-38.02157 20.727581,-27.51879 15.69067,9.39636 7.08818,35.28919 -10.71562,32.25317 z m 6.3763,-1.79357 c 2.47001,-0.7834 6.6668,-3.3467 7.03029,-4.29392 0.69025,-1.79878 -0.81114,-3.12925 -3.82719,-3.39149 -3.27573,-0.28481 -4.48121,-0.99867 -4.58065,-2.71254 -0.0156,-0.26859 -0.14166,0.0177 -0.28016,0.63613 -0.1385,0.61846 -0.37678,1.12448 -0.52951,1.12448 -0.15348,0 -0.21446,-0.26629 -0.13634,-0.59531 0.0777,-0.32742 0.22344,-1.27992 0.32376,-2.11667 0.21586,-1.8006 0.0992,-1.76992 2.10057,-0.55256 1.92939,1.17357 1.8503,1.11078 1.65059,1.31049 -0.0894,0.0894 -0.71959,-0.14869 -1.40034,-0.52917 -1.52875,-0.85444 -1.53103,-0.85438 -1.32294,0.0358 0.49981,2.1381 1.20941,2.71198 3.35333,2.71198 2.81449,0 4.80144,0.94812 5.11201,2.43932 l 0.18078,0.86798 0.66314,-0.79375 c 3.30376,-3.95447 4.48753,-13.45576 1.81556,-14.57218 -1.73309,-0.72413 -3.0602,-0.0157 -4.46677,2.38444 -1.29628,2.21194 -3.11411,3.09004 -4.67149,2.25655 -0.75238,-0.40266 -0.62421,4.1e-4 0.30497,0.95908 0.44921,0.46346 0.70292,0.91301 0.5638,0.99899 -0.23661,0.14623 -3.14156,-2.94029 -2.93213,-3.11542 0.21807,-0.18235 3.53666,-1.21459 3.90483,-1.21459 0.63554,0 0.0225,0.41238 -1.40123,0.94254 l -1.29694,0.48295 0.90007,0.32546 c 1.87365,0.6775 2.99188,0.12809 4.30713,-2.1162 1.20577,-2.05751 2.41587,-2.89889 3.91407,-2.72145 0.7028,0.0832 1.27782,0.10302 1.27782,0.044 0,-1.37102 -2.95783,-5.3592 -5.23317,-7.05612 -6.07717,-4.53229 -9.59988,-3.10352 -6.79232,2.75487 1.10295,2.30146 1.14093,3.15871 0.19492,4.399 l -0.71807,0.94144 0.94774,-0.58574 c 1.03929,-0.64231 1.28215,-0.70774 1.28215,-0.34543 0,0.36332 -3.48546,2.20399 -3.70777,1.95805 -0.31783,-0.3516 -0.56245,-4.05043 -0.26787,-4.05043 0.2888,0 0.48821,0.70709 0.59693,2.11666 l 0.0714,0.92605 0.99219,-0.96573 c 1.23384,-1.20094 1.24341,-1.69999 0.0792,-4.12929 -1.46563,-3.05825 -1.47971,-4.0016 -0.0827,-5.53692 l 0.52574,-0.57776 -0.99385,-0.18644 c -5.099451,-0.95667 -13.962021,2.86562 -13.315561,5.74279 0.46838,2.08462 1.25051,2.60399 3.93138,2.61064 2.52489,0.006 4.05396,0.76683 4.478061,2.22744 l 0.23672,0.81527 0.0239,-0.99219 c 0.0132,-0.54789 0.15793,-0.99219 0.32324,-0.99219 0.30415,0 0.20944,3.81272 -0.10245,4.12461 -0.23495,0.23495 -3.607691,-1.57697 -3.622801,-1.94625 -0.009,-0.2221 0.44152,-0.10451 1.33552,0.34851 1.56746,0.79431 1.65606,0.76145 1.41479,-0.52464 -0.34444,-1.83602 -1.52303,-2.51051 -4.41231,-2.52508 -2.40948,-0.0122 -4.17254,-1.28944 -4.17254,-3.02286 0,-2.02182 -3.0139,3.30102 -3.83055,6.76511 -0.64979,2.75633 -0.40567,7.12883 0.42747,7.65669 1.39672,0.88492 2.81718,0.31796 5.11314,-2.04085 l 2.21658,-2.27728 1.22005,0.097 c 1.23131,0.0979 1.2289,0.0398 -0.0305,-0.73698 -0.26726,-0.16483 -0.40019,-0.38949 -0.29538,-0.49923 0.24317,-0.25464 3.68689,1.97377 3.50501,2.26806 -0.15465,0.25023 -3.14956,2.10274 -3.39945,2.10274 -0.46441,0 -0.033,-0.47894 1.0448,-1.15986 1.38005,-0.87189 1.28003,-1.14426 -0.51029,-1.38965 l -1.26466,-0.17334 -1.96448,2.03584 c -2.18022,2.25941 -3.25936,2.86126 -4.70864,2.62607 l -0.95904,-0.15563 0.31127,0.89423 c 1.36562,3.92307 4.94953,7.47896 9.28772,9.21509 l 1.53279,0.61342 0.64234,-0.60345 c 1.197901,-1.12536 1.310221,-2.0916 0.545371,-4.69133 -0.818331,-2.78156 -0.838901,-3.37798 -0.155151,-4.49941 0.598191,-0.9811 0.549141,-1.01913 -0.56879,-0.44103 -0.59031,0.30526 -0.77939,0.32707 -0.69717,0.0804 0.11977,-0.35933 3.336421,-2.22409 3.519921,-2.04058 0.1696,0.1696 0.57891,3.9148 0.44273,4.05098 -0.30021,0.30021 -0.57746,-0.24183 -0.58117,-1.13624 -0.007,-1.6943 -0.36519,-1.85423 -1.29084,-0.5764 -0.993201,1.37107 -0.995791,1.44676 -0.14519,4.24956 0.77668,2.55925 0.72042,3.58754 -0.26713,4.88229 l -0.674161,0.88386 0.727661,0.12755 c 1.32646,0.2325 5.9997,-0.0587 7.23626,-0.45088 z"
|
||||||
|
id="path3959" /><path
|
||||||
|
style="fill:#d1260c;fill-opacity:1"
|
||||||
|
d="m 101.91223,149.43665 c -0.40929,-0.40929 -0.40929,-1.60154 0,-2.01083 0.17463,-0.17463 0.62707,-0.3175 1.00542,-0.3175 0.37835,0 0.83079,0.14287 1.00542,0.3175 0.17462,0.17462 0.3175,0.62706 0.3175,1.00542 0,0.37835 -0.14288,0.83079 -0.3175,1.00541 -0.17463,0.17463 -0.62707,0.3175 -1.00542,0.3175 -0.37835,0 -0.83079,-0.14287 -1.00542,-0.3175 z"
|
||||||
|
id="path3958" /><circle
|
||||||
|
style="fill:none;fill-opacity:1;stroke:#5a6334;stroke-width:0.288812;stroke-opacity:1"
|
||||||
|
id="path3962"
|
||||||
|
cx="102.92316"
|
||||||
|
cy="148.43149"
|
||||||
|
r="19.699594" /></g></svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 749 B After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 259 KiB |
|
After Width: | Height: | Size: 459 KiB |
|
After Width: | Height: | Size: 678 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 112 KiB |
@@ -0,0 +1,35 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="39.688015mm"
|
||||||
|
height="39.688244mm"
|
||||||
|
viewBox="0 0 39.688015 39.688244"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
xml:space="preserve"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||||
|
id="defs1"><linearGradient
|
||||||
|
id="swatch3962"><stop
|
||||||
|
style="stop-color:#5a6334;stop-opacity:1;"
|
||||||
|
offset="0"
|
||||||
|
id="stop3962" /></linearGradient></defs><g
|
||||||
|
id="layer1"
|
||||||
|
transform="translate(-83.079156,-128.58724)"><circle
|
||||||
|
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.430698;stroke-opacity:1"
|
||||||
|
id="path3963"
|
||||||
|
cx="102.92317"
|
||||||
|
cy="148.43124"
|
||||||
|
r="19.844" /><path
|
||||||
|
style="fill:#5a6334;fill-opacity:1"
|
||||||
|
d="m 101.2073,165.73096 c -4.504241,-0.76809 -7.189631,-2.03794 -10.011961,-4.73438 -14.79928,-14.13914 3.18934,-38.02157 20.727581,-27.51879 15.69067,9.39636 7.08818,35.28919 -10.71562,32.25317 z m 6.3763,-1.79357 c 2.47001,-0.7834 6.6668,-3.3467 7.03029,-4.29392 0.69025,-1.79878 -0.81114,-3.12925 -3.82719,-3.39149 -3.27573,-0.28481 -4.48121,-0.99867 -4.58065,-2.71254 -0.0156,-0.26859 -0.14166,0.0177 -0.28016,0.63613 -0.1385,0.61846 -0.37678,1.12448 -0.52951,1.12448 -0.15348,0 -0.21446,-0.26629 -0.13634,-0.59531 0.0777,-0.32742 0.22344,-1.27992 0.32376,-2.11667 0.21586,-1.8006 0.0992,-1.76992 2.10057,-0.55256 1.92939,1.17357 1.8503,1.11078 1.65059,1.31049 -0.0894,0.0894 -0.71959,-0.14869 -1.40034,-0.52917 -1.52875,-0.85444 -1.53103,-0.85438 -1.32294,0.0358 0.49981,2.1381 1.20941,2.71198 3.35333,2.71198 2.81449,0 4.80144,0.94812 5.11201,2.43932 l 0.18078,0.86798 0.66314,-0.79375 c 3.30376,-3.95447 4.48753,-13.45576 1.81556,-14.57218 -1.73309,-0.72413 -3.0602,-0.0157 -4.46677,2.38444 -1.29628,2.21194 -3.11411,3.09004 -4.67149,2.25655 -0.75238,-0.40266 -0.62421,4.1e-4 0.30497,0.95908 0.44921,0.46346 0.70292,0.91301 0.5638,0.99899 -0.23661,0.14623 -3.14156,-2.94029 -2.93213,-3.11542 0.21807,-0.18235 3.53666,-1.21459 3.90483,-1.21459 0.63554,0 0.0225,0.41238 -1.40123,0.94254 l -1.29694,0.48295 0.90007,0.32546 c 1.87365,0.6775 2.99188,0.12809 4.30713,-2.1162 1.20577,-2.05751 2.41587,-2.89889 3.91407,-2.72145 0.7028,0.0832 1.27782,0.10302 1.27782,0.044 0,-1.37102 -2.95783,-5.3592 -5.23317,-7.05612 -6.07717,-4.53229 -9.59988,-3.10352 -6.79232,2.75487 1.10295,2.30146 1.14093,3.15871 0.19492,4.399 l -0.71807,0.94144 0.94774,-0.58574 c 1.03929,-0.64231 1.28215,-0.70774 1.28215,-0.34543 0,0.36332 -3.48546,2.20399 -3.70777,1.95805 -0.31783,-0.3516 -0.56245,-4.05043 -0.26787,-4.05043 0.2888,0 0.48821,0.70709 0.59693,2.11666 l 0.0714,0.92605 0.99219,-0.96573 c 1.23384,-1.20094 1.24341,-1.69999 0.0792,-4.12929 -1.46563,-3.05825 -1.47971,-4.0016 -0.0827,-5.53692 l 0.52574,-0.57776 -0.99385,-0.18644 c -5.099451,-0.95667 -13.962021,2.86562 -13.315561,5.74279 0.46838,2.08462 1.25051,2.60399 3.93138,2.61064 2.52489,0.006 4.05396,0.76683 4.478061,2.22744 l 0.23672,0.81527 0.0239,-0.99219 c 0.0132,-0.54789 0.15793,-0.99219 0.32324,-0.99219 0.30415,0 0.20944,3.81272 -0.10245,4.12461 -0.23495,0.23495 -3.607691,-1.57697 -3.622801,-1.94625 -0.009,-0.2221 0.44152,-0.10451 1.33552,0.34851 1.56746,0.79431 1.65606,0.76145 1.41479,-0.52464 -0.34444,-1.83602 -1.52303,-2.51051 -4.41231,-2.52508 -2.40948,-0.0122 -4.17254,-1.28944 -4.17254,-3.02286 0,-2.02182 -3.0139,3.30102 -3.83055,6.76511 -0.64979,2.75633 -0.40567,7.12883 0.42747,7.65669 1.39672,0.88492 2.81718,0.31796 5.11314,-2.04085 l 2.21658,-2.27728 1.22005,0.097 c 1.23131,0.0979 1.2289,0.0398 -0.0305,-0.73698 -0.26726,-0.16483 -0.40019,-0.38949 -0.29538,-0.49923 0.24317,-0.25464 3.68689,1.97377 3.50501,2.26806 -0.15465,0.25023 -3.14956,2.10274 -3.39945,2.10274 -0.46441,0 -0.033,-0.47894 1.0448,-1.15986 1.38005,-0.87189 1.28003,-1.14426 -0.51029,-1.38965 l -1.26466,-0.17334 -1.96448,2.03584 c -2.18022,2.25941 -3.25936,2.86126 -4.70864,2.62607 l -0.95904,-0.15563 0.31127,0.89423 c 1.36562,3.92307 4.94953,7.47896 9.28772,9.21509 l 1.53279,0.61342 0.64234,-0.60345 c 1.197901,-1.12536 1.310221,-2.0916 0.545371,-4.69133 -0.818331,-2.78156 -0.838901,-3.37798 -0.155151,-4.49941 0.598191,-0.9811 0.549141,-1.01913 -0.56879,-0.44103 -0.59031,0.30526 -0.77939,0.32707 -0.69717,0.0804 0.11977,-0.35933 3.336421,-2.22409 3.519921,-2.04058 0.1696,0.1696 0.57891,3.9148 0.44273,4.05098 -0.30021,0.30021 -0.57746,-0.24183 -0.58117,-1.13624 -0.007,-1.6943 -0.36519,-1.85423 -1.29084,-0.5764 -0.993201,1.37107 -0.995791,1.44676 -0.14519,4.24956 0.77668,2.55925 0.72042,3.58754 -0.26713,4.88229 l -0.674161,0.88386 0.727661,0.12755 c 1.32646,0.2325 5.9997,-0.0587 7.23626,-0.45088 z"
|
||||||
|
id="path3959" /><path
|
||||||
|
style="fill:#d1260c;fill-opacity:1"
|
||||||
|
d="m 101.91223,149.43665 c -0.40929,-0.40929 -0.40929,-1.60154 0,-2.01083 0.17463,-0.17463 0.62707,-0.3175 1.00542,-0.3175 0.37835,0 0.83079,0.14287 1.00542,0.3175 0.17462,0.17462 0.3175,0.62706 0.3175,1.00542 0,0.37835 -0.14288,0.83079 -0.3175,1.00541 -0.17463,0.17463 -0.62707,0.3175 -1.00542,0.3175 -0.37835,0 -0.83079,-0.14287 -1.00542,-0.3175 z"
|
||||||
|
id="path3958" /><circle
|
||||||
|
style="fill:none;fill-opacity:1;stroke:#5a6334;stroke-width:0.288812;stroke-opacity:1"
|
||||||
|
id="path3962"
|
||||||
|
cx="102.92316"
|
||||||
|
cy="148.43149"
|
||||||
|
r="19.699594" /></g></svg>
|
||||||
|
After Width: | Height: | Size: 5.2 KiB |
@@ -31,7 +31,7 @@ const { locale, dict: t } = Astro.props;
|
|||||||
<div>
|
<div>
|
||||||
<div class="footer__title">{t.footer.contactTitle}</div>
|
<div class="footer__title">{t.footer.contactTitle}</div>
|
||||||
<p>{VENUE.name}</p>
|
<p>{VENUE.name}</p>
|
||||||
<p><a href={localizedPath(locale, '/map')}>{locale === 'en' ? '12 Vifleemska St, Kyiv' : 'вул. Вифлеємська, 12, Київ'}</a></p>
|
<p><a href={localizedPath(locale, '/map')}>{t.about.venueAddress}</a></p>
|
||||||
<p>
|
<p>
|
||||||
<a href={`mailto:${VENUE.email}`}>{VENUE.email}</a>
|
<a href={`mailto:${VENUE.email}`}>{VENUE.email}</a>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { localizedPath } from '../lib/i18n';
|
import { localizedPath } from '../lib/i18n';
|
||||||
import type { Locale, Dict } from '../lib/i18n';
|
import type { Locale, Dict } from '../lib/i18n';
|
||||||
import LangSwitcher from './islands/LangSwitcher.astro';
|
import LangSwitcher from './islands/LangSwitcher.astro';
|
||||||
|
import logo from '../assets/logo.svg';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
locale: Locale;
|
locale: Locale;
|
||||||
@@ -27,7 +28,7 @@ const current = (path: string) =>
|
|||||||
<header class="header">
|
<header class="header">
|
||||||
<div class="container header__inner">
|
<div class="container header__inner">
|
||||||
<a class="logo" href={localizedPath(locale, '/')}>
|
<a class="logo" href={localizedPath(locale, '/')}>
|
||||||
<span class="logo__mark">D</span>
|
<img class="logo__mark" src={logo.src} alt="Logo" fetchpriority="high" />
|
||||||
<span>Devodytyji</span>
|
<span>Devodytyji</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,23 @@
|
|||||||
import { useRef, useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
import type { FormEvent, ChangeEvent } from 'react';
|
import type { FormEvent, ChangeEvent } from 'react';
|
||||||
import { createBrowserSupabase } from '../../lib/supabase';
|
import { createBrowserSupabase } from '../../lib/supabase';
|
||||||
|
import {
|
||||||
|
MDXEditor,
|
||||||
|
headingsPlugin,
|
||||||
|
listsPlugin,
|
||||||
|
linkPlugin,
|
||||||
|
imagePlugin,
|
||||||
|
quotePlugin,
|
||||||
|
thematicBreakPlugin,
|
||||||
|
markdownShortcutPlugin,
|
||||||
|
toolbarPlugin,
|
||||||
|
UndoRedo,
|
||||||
|
BoldItalicUnderlineToggles,
|
||||||
|
ListsToggle,
|
||||||
|
BlockTypeSelect,
|
||||||
|
InsertImage,
|
||||||
|
} from '@mdxeditor/editor';
|
||||||
|
import '@mdxeditor/editor/style.css';
|
||||||
import type { Dict } from '../../lib/i18n';
|
import type { Dict } from '../../lib/i18n';
|
||||||
import type { EventRow } from '../../lib/db';
|
import type { EventRow } from '../../lib/db';
|
||||||
|
|
||||||
@@ -9,6 +26,11 @@ interface Props {
|
|||||||
event?: EventRow | null;
|
event?: EventRow | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ContentEditorProps {
|
||||||
|
content: string;
|
||||||
|
onChange: (content: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
function pad(n: number): string {
|
function pad(n: number): string {
|
||||||
return String(n).padStart(2, '0');
|
return String(n).padStart(2, '0');
|
||||||
}
|
}
|
||||||
@@ -30,6 +52,55 @@ function slugify(value: string): string {
|
|||||||
.slice(0, 80);
|
.slice(0, 80);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function imageUploadHandler(file: File): Promise<string> {
|
||||||
|
try {
|
||||||
|
const ext = file.name.split('.').pop()?.toLowerCase() || 'jpg';
|
||||||
|
const path = `content-images/${crypto.randomUUID()}.${ext}`;
|
||||||
|
const supabase = createBrowserSupabase();
|
||||||
|
const { data, error: uploadError } = await supabase.storage
|
||||||
|
.from('event-content-images')
|
||||||
|
.upload(path, file, { upsert: false, contentType: file.type });
|
||||||
|
|
||||||
|
if (uploadError) throw uploadError;
|
||||||
|
if (!data) throw new Error('No upload path');
|
||||||
|
|
||||||
|
const { data: publicUrl } = supabase.storage.from('event-content-images').getPublicUrl(data.path);
|
||||||
|
return publicUrl.publicUrl;
|
||||||
|
} catch {
|
||||||
|
throw new Error('Image upload failed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContentEditor({ content, onChange }: ContentEditorProps) {
|
||||||
|
return (
|
||||||
|
<MDXEditor
|
||||||
|
markdown={content}
|
||||||
|
onChange={onChange}
|
||||||
|
plugins={[
|
||||||
|
headingsPlugin(),
|
||||||
|
listsPlugin(),
|
||||||
|
linkPlugin(),
|
||||||
|
imagePlugin({ imageUploadHandler: imageUploadHandler }),
|
||||||
|
quotePlugin(),
|
||||||
|
thematicBreakPlugin(),
|
||||||
|
markdownShortcutPlugin(),
|
||||||
|
toolbarPlugin({
|
||||||
|
toolbarContents: () => (
|
||||||
|
<>
|
||||||
|
<UndoRedo />
|
||||||
|
<BoldItalicUnderlineToggles />
|
||||||
|
<BlockTypeSelect />
|
||||||
|
<ListsToggle />
|
||||||
|
|
||||||
|
<InsertImage />
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
]}
|
||||||
|
contentEditableClassName="prose prose-sm max-w-none textarea"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
export default function EventEditor({ dict: t, event }: Props) {
|
export default function EventEditor({ dict: t, event }: Props) {
|
||||||
const [titleUa, setTitleUa] = useState(event?.title?.ua ?? '');
|
const [titleUa, setTitleUa] = useState(event?.title?.ua ?? '');
|
||||||
const [titleEn, setTitleEn] = useState(event?.title?.en ?? '');
|
const [titleEn, setTitleEn] = useState(event?.title?.en ?? '');
|
||||||
@@ -94,6 +165,10 @@ export default function EventEditor({ dict: t, event }: Props) {
|
|||||||
setError(t.common.error);
|
setError(t.common.error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (descriptionUa.trim().length > 20000 || descriptionEn.trim().length > 20000) {
|
||||||
|
setError(t.common.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
title: { ua: titleUa.trim(), en: titleEn.trim() },
|
title: { ua: titleUa.trim(), en: titleEn.trim() },
|
||||||
@@ -158,25 +233,13 @@ export default function EventEditor({ dict: t, event }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid-2">
|
<div className="form-field">
|
||||||
<div className="form-field">
|
<label htmlFor="desc-ua">{t.admin.eventDescUa}</label>
|
||||||
<label htmlFor="desc-ua">{t.admin.eventDescUa}</label>
|
<ContentEditor content={descriptionUa} onChange={setDescriptionUa} />
|
||||||
<textarea
|
</div>
|
||||||
className="textarea"
|
<div className="form-field">
|
||||||
id="desc-ua"
|
<label htmlFor="desc-en">{t.admin.eventDescEn}</label>
|
||||||
value={descriptionUa}
|
<ContentEditor content={descriptionEn} onChange={setDescriptionEn} />
|
||||||
onChange={(e) => setDescriptionUa(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="form-field">
|
|
||||||
<label htmlFor="desc-en">{t.admin.eventDescEn}</label>
|
|
||||||
<textarea
|
|
||||||
className="textarea"
|
|
||||||
id="desc-en"
|
|
||||||
value={descriptionEn}
|
|
||||||
onChange={(e) => setDescriptionEn(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid-2">
|
<div className="grid-2">
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export default function MapView({ lat, lng, name, address, hint }: Props) {
|
|||||||
return (
|
return (
|
||||||
<MapContainer
|
<MapContainer
|
||||||
center={[lat, lng]}
|
center={[lat, lng]}
|
||||||
zoom={15}
|
zoom={12}
|
||||||
scrollWheelZoom={false}
|
scrollWheelZoom={false}
|
||||||
style={{ height: '100%', width: '100%' }}
|
style={{ height: '100%', width: '100%' }}
|
||||||
aria-label={hint}
|
aria-label={hint}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
---
|
---
|
||||||
import '../styles/global.css';
|
import '../styles/global.scss';
|
||||||
|
import logo from '../assets/logo.svg';
|
||||||
import { getDict } from '../lib/i18n';
|
import { getDict } from '../lib/i18n';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -35,7 +36,7 @@ const navItems = [
|
|||||||
<header class="header">
|
<header class="header">
|
||||||
<div class="container header__inner">
|
<div class="container header__inner">
|
||||||
<a class="logo" href="/admin">
|
<a class="logo" href="/admin">
|
||||||
<span class="logo__mark">D</span>
|
<img class="logo__mark" src={logo.src} alt="Logo" fetchpriority="high" />
|
||||||
<span>Devodytyji / Admin</span>
|
<span>Devodytyji / Admin</span>
|
||||||
</a>
|
</a>
|
||||||
<nav class="nav" aria-label="admin">
|
<nav class="nav" aria-label="admin">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
import '../styles/global.css';
|
import '../styles/global.scss';
|
||||||
import { LOCALES, localizedPath, getAlternateLocales } from '../lib/i18n';
|
import { LOCALES, localizedPath, getAlternateLocales } from '../lib/i18n';
|
||||||
import type { Locale, Dict } from '../lib/i18n';
|
import type { Locale, Dict } from '../lib/i18n';
|
||||||
import Header from '../components/Header.astro';
|
import Header from '../components/Header.astro';
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ export const en: Dict = {
|
|||||||
homeDescription:
|
homeDescription:
|
||||||
'Devodytyji is an open-air cultural space: concerts, movies, talks and markets under the open sky.',
|
'Devodytyji is an open-air cultural space: concerts, movies, talks and markets under the open sky.',
|
||||||
defaultOgImage: '/og-cover.jpg',
|
defaultOgImage: '/og-cover.jpg',
|
||||||
|
notFoundTitle: 'Page not found',
|
||||||
|
notFoundDescription: 'The page you are looking for does not exist or has been moved.',
|
||||||
},
|
},
|
||||||
nav: {
|
nav: {
|
||||||
home: 'Home',
|
home: 'Home',
|
||||||
@@ -100,6 +102,8 @@ export const en: Dict = {
|
|||||||
storyTitle: 'Our story',
|
storyTitle: 'Our story',
|
||||||
story:
|
story:
|
||||||
'Devodytyji began as an ordinary yard where neighbors gathered. Over time it became an open cultural venue: music, movies under the stars, talks, fairs and workshops. We believe culture lives best in the open air — among people, under the open sky.',
|
'Devodytyji began as an ordinary yard where neighbors gathered. Over time it became an open cultural venue: music, movies under the stars, talks, fairs and workshops. We believe culture lives best in the open air — among people, under the open sky.',
|
||||||
|
venueAddress: 'Vodotyi, Zhytomyr Oblast',
|
||||||
|
venueAddressLong: 'Vodotyi, Zhytomyr Oblast, 12624',
|
||||||
venueReviewsTitle: 'Venue reviews',
|
venueReviewsTitle: 'Venue reviews',
|
||||||
venueReviewCount: 'reviews',
|
venueReviewCount: 'reviews',
|
||||||
writeVenueReview: 'Leave a review about the venue',
|
writeVenueReview: 'Leave a review about the venue',
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ export const ua = {
|
|||||||
homeDescription:
|
homeDescription:
|
||||||
'Devodytyji — відкритий простір на свіжому повітрі: концерти, кіно, лекції та маркети просто неба.',
|
'Devodytyji — відкритий простір на свіжому повітрі: концерти, кіно, лекції та маркети просто неба.',
|
||||||
defaultOgImage: '/og-cover.jpg',
|
defaultOgImage: '/og-cover.jpg',
|
||||||
|
notFoundTitle: 'Сторінку не знайдено',
|
||||||
|
notFoundDescription: 'Схоже, такої сторінки не існує або її було переміщено.',
|
||||||
},
|
},
|
||||||
nav: {
|
nav: {
|
||||||
home: 'Головна',
|
home: 'Головна',
|
||||||
@@ -98,11 +100,13 @@ export const ua = {
|
|||||||
storyTitle: 'Наша історія',
|
storyTitle: 'Наша історія',
|
||||||
story:
|
story:
|
||||||
'Devodytyji починався як звичайний двір, де збиралися сусіди. З часом він перетворився на відкритий культурний майданчик: тут грають музику, показують кіно під зорями, проводять лекції, ярмарки та майстер-класи. Ми віримо, що культура найкраще живе просто неба — під відкритим небом, у колі людей.',
|
'Devodytyji починався як звичайний двір, де збиралися сусіди. З часом він перетворився на відкритий культурний майданчик: тут грають музику, показують кіно під зорями, проводять лекції, ярмарки та майстер-класи. Ми віримо, що культура найкраще живе просто неба — під відкритим небом, у колі людей.',
|
||||||
|
venueAddress: 'Водотиї, Житомирська область',
|
||||||
|
venueAddressLong: 'село Водотиї, Житомирська область, 12624',
|
||||||
venueReviewsTitle: 'Відгуки про майданчик',
|
venueReviewsTitle: 'Відгуки про майданчик',
|
||||||
venueReviewCount: 'відгуків',
|
venueReviewCount: 'відгуків',
|
||||||
writeVenueReview: 'Залишити відгук про майданчик',
|
writeVenueReview: 'Залишити відгук про майданчик',
|
||||||
noVenueReviews: 'Відгуків поки немає. Будьте першим!',
|
noVenueReviews: 'Відгуків поки немає. Будьте першим!',
|
||||||
contactsTitle: 'Контакти',
|
contactsTitle: 'Контакти'
|
||||||
},
|
},
|
||||||
contacts: {
|
contacts: {
|
||||||
pageTitle: 'Контакти',
|
pageTitle: 'Контакти',
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import markdownIt from 'markdown-it';
|
||||||
|
import DOMPurify from 'isomorphic-dompurify';
|
||||||
|
|
||||||
|
const md = markdownIt({
|
||||||
|
html: false,
|
||||||
|
linkify: true,
|
||||||
|
typographer: true,
|
||||||
|
breaks: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function markdownToHtml(markdown: string): Promise<string> {
|
||||||
|
if (!markdown) return '';
|
||||||
|
const rendered = md.render(markdown);
|
||||||
|
return DOMPurify.sanitize(rendered);
|
||||||
|
}
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
import type { Locale } from './i18n';
|
|
||||||
|
|
||||||
export interface VenueInfo {
|
export interface VenueInfo {
|
||||||
name: string;
|
name: string;
|
||||||
lat: number;
|
lat: number;
|
||||||
@@ -11,13 +9,9 @@ export interface VenueInfo {
|
|||||||
|
|
||||||
export const VENUE: VenueInfo = {
|
export const VENUE: VenueInfo = {
|
||||||
name: 'Devodytyji',
|
name: 'Devodytyji',
|
||||||
lat: 50.4584,
|
lat: 50.2384827,
|
||||||
lng: 30.5199,
|
lng: 29.439712,
|
||||||
phone: '+380 44 000 00 00',
|
phone: '+380 95 392 82 36',
|
||||||
email: 'hello@devodytyji.example',
|
email: 'hello@devodytyji.example',
|
||||||
hours: '10:00 – 23:00',
|
hours: '10:00 – 23:00',
|
||||||
};
|
};
|
||||||
|
|
||||||
export function venueAddress(locale: Locale): string {
|
|
||||||
return locale === 'en' ? '12 Vifleemska St, Kyiv, Ukraine' : 'вул. Вифлеємська, 12, Київ, Україна';
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
---
|
||||||
|
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||||
|
import { getDict, getLocaleFromUrl, localizedPath } from '../lib/i18n';
|
||||||
|
import type { Locale } from '../lib/i18n';
|
||||||
|
|
||||||
|
const locale: Locale = Astro.locals.locale ?? getLocaleFromUrl(Astro.url);
|
||||||
|
const dict = getDict(locale);
|
||||||
|
const user = Astro.locals.user;
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout
|
||||||
|
locale={locale}
|
||||||
|
dict={dict}
|
||||||
|
pagePath="/404"
|
||||||
|
user={user}
|
||||||
|
title={`${dict.meta.notFoundTitle} — ${dict.meta.siteName}`}
|
||||||
|
description={dict.meta.notFoundDescription}
|
||||||
|
>
|
||||||
|
<section class="section">
|
||||||
|
<div class="container" style="text-align:center;">
|
||||||
|
<span class="eyebrow">404</span>
|
||||||
|
<h1>{dict.meta.notFoundTitle}</h1>
|
||||||
|
<p class="text-muted">{dict.meta.notFoundDescription}</p>
|
||||||
|
<p>
|
||||||
|
<a class="btn btn--primary" href={localizedPath(locale, '/')}>{dict.nav.home}</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</BaseLayout>
|
||||||
@@ -40,7 +40,7 @@ const canDelete = (reviewUserId: string) => user !== null && (user.isAdmin || re
|
|||||||
<span><Icon name="pin" size={18} /></span>
|
<span><Icon name="pin" size={18} /></span>
|
||||||
<div>
|
<div>
|
||||||
<div class="contact-detail__label">{dict.contacts.addressLabel}</div>
|
<div class="contact-detail__label">{dict.contacts.addressLabel}</div>
|
||||||
{locale === 'en' ? '12 Vifleemska St, Kyiv, Ukraine' : 'вул. Вифлеємська, 12, Київ, Україна'}
|
{dict.about.venueAddress}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="contact-detail">
|
<div class="contact-detail">
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import MapView from '../../components/islands/MapView';
|
|||||||
import Icon from '../../components/ui/Icon.astro';
|
import Icon from '../../components/ui/Icon.astro';
|
||||||
import { getDict } from '../../lib/i18n';
|
import { getDict } from '../../lib/i18n';
|
||||||
import type { Locale } from '../../lib/i18n';
|
import type { Locale } from '../../lib/i18n';
|
||||||
import { VENUE, venueAddress } from '../../lib/venue';
|
import { VENUE } from '../../lib/venue';
|
||||||
|
|
||||||
const { lang } = Astro.params;
|
const { lang } = Astro.params;
|
||||||
const locale = (lang ?? 'ua') as Locale;
|
const locale = (lang ?? 'ua') as Locale;
|
||||||
@@ -45,7 +45,7 @@ const user = Astro.locals.user;
|
|||||||
<span><Icon name="pin" size={18} /></span>
|
<span><Icon name="pin" size={18} /></span>
|
||||||
<div>
|
<div>
|
||||||
<div class="contact-detail__label">{dict.contacts.addressLabel}</div>
|
<div class="contact-detail__label">{dict.contacts.addressLabel}</div>
|
||||||
<strong>{venueAddress(locale)}</strong>
|
<strong>{dict.about.venueAddressLong}</strong>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="contact-detail">
|
<div class="contact-detail">
|
||||||
@@ -77,7 +77,7 @@ const user = Astro.locals.user;
|
|||||||
lat={VENUE.lat}
|
lat={VENUE.lat}
|
||||||
lng={VENUE.lng}
|
lng={VENUE.lng}
|
||||||
name={VENUE.name}
|
name={VENUE.name}
|
||||||
address={venueAddress(locale)}
|
address={dict.about.venueAddressLong}
|
||||||
hint={dict.map.hint}
|
hint={dict.map.hint}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { localizedPath, getDict, isLocale } from '../../../lib/i18n';
|
|||||||
import type { Locale } from '../../../lib/i18n';
|
import type { Locale } from '../../../lib/i18n';
|
||||||
import { getEventBySlug, getEventReviews, titleFor, descriptionFor } from '../../../lib/db';
|
import { getEventBySlug, getEventReviews, titleFor, descriptionFor } from '../../../lib/db';
|
||||||
import { formatDate, formatTime } from '../../../lib/format';
|
import { formatDate, formatTime } from '../../../lib/format';
|
||||||
|
import { markdownToHtml } from '../../../lib/markdown';
|
||||||
|
|
||||||
const { lang, slug } = Astro.params;
|
const { lang, slug } = Astro.params;
|
||||||
const locale: Locale = isLocale(lang) ? lang : 'ua';
|
const locale: Locale = isLocale(lang) ? lang : 'ua';
|
||||||
@@ -22,6 +23,7 @@ if (!event) {
|
|||||||
|
|
||||||
const title = event ? titleFor(locale, event) : '404';
|
const title = event ? titleFor(locale, event) : '404';
|
||||||
const description = event ? descriptionFor(locale, event) : 'Not found';
|
const description = event ? descriptionFor(locale, event) : 'Not found';
|
||||||
|
const descriptionHtml = event ? await markdownToHtml(description) : '';
|
||||||
const reviews = event ? await getEventReviews(supabase, event.id) : [];
|
const reviews = event ? await getEventReviews(supabase, event.id) : [];
|
||||||
const canDelete = (reviewUserId: string) => user !== null && (user.isAdmin || reviewUserId === user.id);
|
const canDelete = (reviewUserId: string) => user !== null && (user.isAdmin || reviewUserId === user.id);
|
||||||
---
|
---
|
||||||
@@ -75,7 +77,7 @@ const canDelete = (reviewUserId: string) => user !== null && (user.isAdmin || re
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div class="prose">{description && <p>{description}</p>}</div>
|
<div class="prose" set:html={descriptionHtml}></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<aside class="card" style="padding:24px;">
|
<aside class="card" style="padding:24px;">
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import MapView from '../../components/islands/MapView';
|
|||||||
import Icon from '../../components/ui/Icon.astro';
|
import Icon from '../../components/ui/Icon.astro';
|
||||||
import { getDict } from '../../lib/i18n';
|
import { getDict } from '../../lib/i18n';
|
||||||
import type { Locale } from '../../lib/i18n';
|
import type { Locale } from '../../lib/i18n';
|
||||||
import { VENUE, venueAddress } from '../../lib/venue';
|
import { VENUE } from '../../lib/venue';
|
||||||
|
|
||||||
const { lang } = Astro.params;
|
const { lang } = Astro.params;
|
||||||
const locale = (lang ?? 'ua') as Locale;
|
const locale = (lang ?? 'ua') as Locale;
|
||||||
@@ -37,7 +37,7 @@ const user = Astro.locals.user;
|
|||||||
<span><Icon name="pin" size={18} /></span>
|
<span><Icon name="pin" size={18} /></span>
|
||||||
<div>
|
<div>
|
||||||
<div class="contact-detail__label">{dict.contacts.addressLabel}</div>
|
<div class="contact-detail__label">{dict.contacts.addressLabel}</div>
|
||||||
<strong>{venueAddress(locale)}</strong>
|
<strong>{dict.about.venueAddressLong}</strong>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="contact-detail">
|
<div class="contact-detail">
|
||||||
@@ -70,7 +70,7 @@ const user = Astro.locals.user;
|
|||||||
lat={VENUE.lat}
|
lat={VENUE.lat}
|
||||||
lng={VENUE.lng}
|
lng={VENUE.lng}
|
||||||
name={VENUE.name}
|
name={VENUE.name}
|
||||||
address={venueAddress(locale)}
|
address={dict.about.venueAddressLong}
|
||||||
hint={dict.map.hint}
|
hint={dict.map.hint}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
// ============================================================
|
||||||
|
// Admin
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
.table-wrap {
|
||||||
|
overflow-x: auto;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
background: var(--surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 0.92rem;
|
||||||
|
|
||||||
|
th,
|
||||||
|
td {
|
||||||
|
padding: var(--space-3) var(--space-4);
|
||||||
|
text-align: left;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
background: var(--bg-alt);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.07em;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
tr + tr td {
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-2);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
.btn {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-shell {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 220px 1fr;
|
||||||
|
gap: var(--space-6);
|
||||||
|
padding-block: var(--space-8);
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-nav {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-1);
|
||||||
|
position: sticky;
|
||||||
|
top: calc(var(--header-h) + var(--space-4));
|
||||||
|
|
||||||
|
a {
|
||||||
|
padding: 0.6rem 0.9rem;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
color: var(--text);
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--bg-alt);
|
||||||
|
}
|
||||||
|
|
||||||
|
&[aria-current='page'] {
|
||||||
|
background: var(--primary);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: var(--space-4);
|
||||||
|
margin-bottom: var(--space-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
padding: var(--space-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card__value {
|
||||||
|
font-family: var(--font-serif);
|
||||||
|
font-size: 2.2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card__label {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
// ============================================================
|
||||||
|
// Auth
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
.auth-wrap {
|
||||||
|
max-width: 420px;
|
||||||
|
margin-inline: auto;
|
||||||
|
padding-block: var(--space-8) var(--space-10);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-card {
|
||||||
|
padding: var(--space-8);
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.oauth-buttons {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.oauth-divider {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-3);
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
|
||||||
|
&::before,
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
flex: 1;
|
||||||
|
height: 1px;
|
||||||
|
background: var(--border);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
// ============================================================
|
||||||
|
// Base & reset
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.6;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4 {
|
||||||
|
font-family: var(--font-serif);
|
||||||
|
color: var(--ink);
|
||||||
|
line-height: 1.2;
|
||||||
|
margin: 0 0 0.5em;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: clamp(2rem, 5vw, 3.25rem);
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
font-size: clamp(1.5rem, 3.2vw, 2.1rem);
|
||||||
|
}
|
||||||
|
h3 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0 0 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--primary);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
color: var(--primary-strong);
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
:focus-visible {
|
||||||
|
outline: 3px solid var(--accent);
|
||||||
|
outline-offset: 2px;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
// ============================================================
|
||||||
|
// Buttons
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
font: inherit;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 0.65rem 1.25rem;
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
border: 2px solid transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
transition:
|
||||||
|
background 0.15s ease,
|
||||||
|
color 0.15s ease,
|
||||||
|
border-color 0.15s ease,
|
||||||
|
transform 0.1s ease;
|
||||||
|
line-height: 1.2;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: translateY(1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.55;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn--primary {
|
||||||
|
background: var(--primary);
|
||||||
|
color: #fff;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--primary-strong);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn--accent {
|
||||||
|
background: var(--accent);
|
||||||
|
color: #fff;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--accent-strong);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn--outline {
|
||||||
|
background: transparent;
|
||||||
|
border-color: var(--primary);
|
||||||
|
color: var(--primary);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--primary-soft);
|
||||||
|
color: var(--primary-strong);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn--ghost {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--bg-alt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn--danger {
|
||||||
|
background: var(--danger);
|
||||||
|
color: #fff;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
filter: brightness(0.92);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn--sm {
|
||||||
|
padding: 0.4rem 0.9rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn--block {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
// ============================================================
|
||||||
|
// Cards & badges
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
overflow: hidden;
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
transition:
|
||||||
|
transform 0.18s ease,
|
||||||
|
box-shadow 0.18s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-card__media {
|
||||||
|
position: relative;
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
|
background: var(--bg-alt);
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-card__media-placeholder {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
font-family: var(--font-serif);
|
||||||
|
font-size: 2rem;
|
||||||
|
color: var(--primary);
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 30% 20%, var(--primary-soft), transparent 60%),
|
||||||
|
var(--bg-alt);
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-card__date-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: var(--space-3);
|
||||||
|
left: var(--space-3);
|
||||||
|
background: var(--accent);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
padding: 0.35rem 0.7rem;
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-card__body {
|
||||||
|
padding: var(--space-5);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-2);
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-card__title {
|
||||||
|
font-family: var(--font-serif);
|
||||||
|
font-size: 1.3rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--ink);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-card__meta {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--space-2) var(--space-4);
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-card__foot {
|
||||||
|
margin-top: auto;
|
||||||
|
padding-top: var(--space-3);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.35rem;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
padding: 0.28rem 0.7rem;
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge--published {
|
||||||
|
background: var(--primary-soft);
|
||||||
|
color: var(--primary-strong);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge--draft {
|
||||||
|
background: var(--accent-soft);
|
||||||
|
color: var(--accent-strong);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge--past {
|
||||||
|
background: var(--bg-alt);
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
// ============================================================
|
||||||
|
// Contact
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
.contact-details {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-detail {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-3);
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-detail__label {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
color: var(--muted);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
// ============================================================
|
||||||
|
// Event detail
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
.event-hero {
|
||||||
|
position: relative;
|
||||||
|
padding-block: var(--space-8) var(--space-10);
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-hero__media {
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
overflow: hidden;
|
||||||
|
aspect-ratio: 21 / 9;
|
||||||
|
background: var(--bg-alt);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
margin-bottom: var(--space-6);
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-hero__body {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 340px;
|
||||||
|
gap: var(--space-8);
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-facts {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-fact {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-3);
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-fact__icon {
|
||||||
|
font-size: 1.15rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-fact__label {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
color: var(--muted);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-fact__value {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose {
|
||||||
|
color: var(--text);
|
||||||
|
|
||||||
|
p {
|
||||||
|
white-space: pre-line;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
// ============================================================
|
||||||
|
// Filters & tabs
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
.filter-bar {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-3);
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: var(--space-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs {
|
||||||
|
display: inline-flex;
|
||||||
|
background: var(--bg-alt);
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
padding: 3px;
|
||||||
|
gap: 2px;
|
||||||
|
|
||||||
|
button {
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
font: inherit;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
padding: 0.45rem 1rem;
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--text);
|
||||||
|
|
||||||
|
&.is-active {
|
||||||
|
background: var(--surface);
|
||||||
|
color: var(--primary-strong);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 220px;
|
||||||
|
max-width: 360px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
// ============================================================
|
||||||
|
// Footer
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
background: var(--ink);
|
||||||
|
color: rgb(255 255 255 / 0.75);
|
||||||
|
padding-block: var(--space-8) var(--space-6);
|
||||||
|
margin-top: var(--space-10);
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: rgb(255 255 255 / 0.8);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer__grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 2fr 1fr 1fr;
|
||||||
|
gap: var(--space-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer__title {
|
||||||
|
font-family: var(--font-serif);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 1.05rem;
|
||||||
|
margin-bottom: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer__bottom {
|
||||||
|
margin-top: var(--space-8);
|
||||||
|
padding-top: var(--space-4);
|
||||||
|
border-top: 1px solid rgb(255 255 255 / 0.15);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
// ============================================================
|
||||||
|
// Forms
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
.form {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field label,
|
||||||
|
.form-field__label {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input,
|
||||||
|
.textarea,
|
||||||
|
.select {
|
||||||
|
font: inherit;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.7rem 0.9rem;
|
||||||
|
border: 1.5px solid var(--border-strong);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: var(--surface);
|
||||||
|
color: var(--ink);
|
||||||
|
transition:
|
||||||
|
border-color 0.15s ease,
|
||||||
|
box-shadow 0.15s ease;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--primary);
|
||||||
|
box-shadow: 0 0 0 3px var(--primary-soft);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.textarea {
|
||||||
|
min-height: 130px;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-error {
|
||||||
|
background: var(--danger-soft);
|
||||||
|
color: var(--danger);
|
||||||
|
padding: var(--space-3) var(--space-4);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-success {
|
||||||
|
background: var(--primary-soft);
|
||||||
|
color: var(--primary-strong);
|
||||||
|
padding: var(--space-3) var(--space-4);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-error {
|
||||||
|
color: var(--danger);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.honeypot {
|
||||||
|
position: absolute;
|
||||||
|
left: -9999px;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
// ============================================================
|
||||||
|
// Header & navigation
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
.header {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 50;
|
||||||
|
background: rgb(250 246 239 / 0.92);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header__inner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-4);
|
||||||
|
min-height: var(--header-h);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
font-family: var(--font-serif);
|
||||||
|
font-size: 1.35rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--ink);
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo__mark {
|
||||||
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-1);
|
||||||
|
margin-left: auto;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav__link {
|
||||||
|
padding: 0.5rem 0.85rem;
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
color: var(--text);
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--bg-alt);
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
&[aria-current='page'] {
|
||||||
|
background: var(--primary-soft);
|
||||||
|
color: var(--primary-strong);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav__sep {
|
||||||
|
width: 1px;
|
||||||
|
height: 22px;
|
||||||
|
background: var(--border-strong);
|
||||||
|
margin-inline: var(--space-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-switch {
|
||||||
|
display: inline-flex;
|
||||||
|
border: 1px solid var(--border-strong);
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--surface);
|
||||||
|
|
||||||
|
a {
|
||||||
|
padding: 0.35rem 0.75rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--muted);
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&[aria-current='true'] {
|
||||||
|
background: var(--primary);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
// ============================================================
|
||||||
|
// Hero
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
.hero {
|
||||||
|
position: relative;
|
||||||
|
padding-block: var(--space-12);
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
background-image: url('/src/assets/bg_1280.webp');
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
|
||||||
|
@media (min-width: 1920px) {
|
||||||
|
background-image: url('/src/assets/bg_2560.webp');
|
||||||
|
}
|
||||||
|
@media (min-width: 1280px) and (max-width: 1919px) {
|
||||||
|
background-image: url('/src/assets/bg_1920.webp');
|
||||||
|
}
|
||||||
|
@media (min-width: 768px) and (max-width: 1279px) {
|
||||||
|
background-image: url('/src/assets/bg_1280.webp');
|
||||||
|
}
|
||||||
|
@media (min-width: 480px) and (max-width: 767px) {
|
||||||
|
background-image: url('/src/assets/bg_768.webp');
|
||||||
|
}
|
||||||
|
@media (max-width: 479px) {
|
||||||
|
background-image: url('/src/assets/bg_480.webp');
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgb(250 246 239 / 0.87) 0%, rgb(250 246 239 / 0.81) 60%, rgb(250 246 239 / 0.91) 100%),
|
||||||
|
radial-gradient(60% 60% at 85% 15%, rgb(217 108 63 / 0.16), transparent 70%),
|
||||||
|
radial-gradient(50% 50% at 10% 90%, rgb(46 107 79 / 0.18), transparent 70%);
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
margin-bottom: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
font-size: 1.15rem;
|
||||||
|
color: var(--text);
|
||||||
|
max-width: 60ch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero__inner {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
max-width: 720px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero__actions {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-3);
|
||||||
|
margin-top: var(--space-6);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
// ============================================================
|
||||||
|
// Layout
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: var(--container);
|
||||||
|
margin-inline: auto;
|
||||||
|
padding-inline: var(--space-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
padding-block: var(--space-10);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section--alt {
|
||||||
|
background: var(--bg-alt);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--space-4);
|
||||||
|
margin-bottom: var(--space-6);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-head p {
|
||||||
|
color: var(--muted);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eyebrow {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--accent-strong);
|
||||||
|
margin-bottom: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.split {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: var(--space-8);
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-3 {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: var(--space-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-2 {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: var(--space-5);
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
// ============================================================
|
||||||
|
// Map
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
.map-wrap {
|
||||||
|
height: 420px;
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-page {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 320px 1fr;
|
||||||
|
gap: var(--space-6);
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-container {
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
// ============================================================
|
||||||
|
// Misc components
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: var(--space-10) var(--space-6);
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state__icon {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
margin-bottom: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-band {
|
||||||
|
background: var(--primary);
|
||||||
|
color: #fff;
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: var(--space-8);
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: rgb(255 255 255 / 0.85);
|
||||||
|
max-width: 52ch;
|
||||||
|
margin-inline: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn--accent {
|
||||||
|
background: var(--accent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
border-radius: 50%;
|
||||||
|
object-fit: cover;
|
||||||
|
background: var(--primary-soft);
|
||||||
|
color: var(--primary-strong);
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
// ============================================================
|
||||||
|
// Responsive
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
@media (max-width: 920px) {
|
||||||
|
.grid-3 {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-hero__body,
|
||||||
|
.split,
|
||||||
|
.map-page,
|
||||||
|
.admin-shell {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-wrap {
|
||||||
|
height: 320px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer__grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: var(--space-6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 620px) {
|
||||||
|
.grid-3,
|
||||||
|
.grid-2 {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
padding-block: var(--space-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header__inner {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
padding-block: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav {
|
||||||
|
order: 3;
|
||||||
|
width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin-left: 0;
|
||||||
|
padding-bottom: var(--space-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
// ============================================================
|
||||||
|
// Reviews
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
.review {
|
||||||
|
padding: var(--space-5);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
background: var(--surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.review__head {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-3);
|
||||||
|
margin-bottom: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.review__author {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.review__date {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stars {
|
||||||
|
color: var(--accent);
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
font-size: 1.05rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.star-rating {
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 0.2rem;
|
||||||
|
|
||||||
|
button {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 1.6rem;
|
||||||
|
line-height: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--border-strong);
|
||||||
|
transition: color 0.1s ease;
|
||||||
|
padding: 0.1rem;
|
||||||
|
|
||||||
|
&.is-selected {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.review-list {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-4);
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
// ============================================================
|
||||||
|
// Design tokens
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--bg: #faf6ef;
|
||||||
|
--bg-alt: #f2ead9;
|
||||||
|
--surface: #ffffff;
|
||||||
|
--ink: #1d2a24;
|
||||||
|
--text: #3f4c44;
|
||||||
|
--muted: #6d766f;
|
||||||
|
--primary: #2e6b4f;
|
||||||
|
--primary-strong: #22543d;
|
||||||
|
--primary-soft: #e4efe8;
|
||||||
|
--accent: #d96c3f;
|
||||||
|
--accent-strong: #b8522b;
|
||||||
|
--accent-soft: #f9e6d8;
|
||||||
|
--danger: #b3392f;
|
||||||
|
--danger-soft: #fbe3e0;
|
||||||
|
--border: #e4d9c5;
|
||||||
|
--border-strong: #d3c5a9;
|
||||||
|
|
||||||
|
--radius-sm: 6px;
|
||||||
|
--radius: 12px;
|
||||||
|
--radius-lg: 20px;
|
||||||
|
--radius-full: 999px;
|
||||||
|
|
||||||
|
--shadow-sm: 0 1px 2px rgb(29 42 36 / 0.06);
|
||||||
|
--shadow: 0 4px 16px rgb(29 42 36 / 0.08);
|
||||||
|
--shadow-lg: 0 12px 40px rgb(29 42 36 / 0.14);
|
||||||
|
|
||||||
|
--font-sans: 'Inter', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
--font-serif: 'Fraunces', Georgia, 'Times New Roman', serif;
|
||||||
|
|
||||||
|
--container: 1160px;
|
||||||
|
--space-1: 0.25rem;
|
||||||
|
--space-2: 0.5rem;
|
||||||
|
--space-3: 0.75rem;
|
||||||
|
--space-4: 1rem;
|
||||||
|
--space-5: 1.5rem;
|
||||||
|
--space-6: 2rem;
|
||||||
|
--space-8: 3rem;
|
||||||
|
--space-10: 4rem;
|
||||||
|
--space-12: 5rem;
|
||||||
|
|
||||||
|
--header-h: 68px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
// ============================================================
|
||||||
|
// Utilities
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
.mt-6 {
|
||||||
|
margin-top: var(--space-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mb-2 {
|
||||||
|
margin-bottom: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mb-6 {
|
||||||
|
margin-bottom: var(--space-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-muted {
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sr-only {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
padding: 0;
|
||||||
|
margin: -1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
white-space: nowrap;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
// ============================================================
|
||||||
|
@use 'tailwind';
|
||||||
|
|
||||||
|
// Devodytyji — global styles entry
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
@use 'tokens';
|
||||||
|
@use 'base';
|
||||||
|
@use 'layout';
|
||||||
|
@use 'buttons';
|
||||||
|
@use 'cards';
|
||||||
|
@use 'header';
|
||||||
|
@use 'footer';
|
||||||
|
@use 'forms';
|
||||||
|
@use 'auth';
|
||||||
|
@use 'reviews';
|
||||||
|
@use 'event-detail';
|
||||||
|
@use 'map';
|
||||||
|
@use 'contact';
|
||||||
|
@use 'admin';
|
||||||
|
@use 'hero';
|
||||||
|
@use 'filters';
|
||||||
|
@use 'misc';
|
||||||
|
@use 'utilities';
|
||||||
|
@use 'responsive';
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
-- Create storage bucket for rich text content images
|
||||||
|
insert into storage.buckets (id, name, public)
|
||||||
|
values ('event-content-images', 'event-content-images', true)
|
||||||
|
on conflict (id) do nothing;
|
||||||
|
|
||||||
|
-- Allow public read access to content images
|
||||||
|
create policy "content_images_select_public" on storage.objects
|
||||||
|
for select
|
||||||
|
using (bucket_id = 'event-content-images');
|
||||||
|
|
||||||
|
-- Allow authenticated admins to upload content images
|
||||||
|
create policy "content_images_insert_admin" on storage.objects
|
||||||
|
for insert
|
||||||
|
with check (
|
||||||
|
bucket_id = 'event-content-images'
|
||||||
|
and auth.role() = 'authenticated'
|
||||||
|
and exists (
|
||||||
|
select 1 from public.profiles p
|
||||||
|
where p.id = auth.uid()
|
||||||
|
and p.is_admin
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Allow authenticated admins to update content images
|
||||||
|
create policy "content_images_update_admin" on storage.objects
|
||||||
|
for update
|
||||||
|
using (
|
||||||
|
bucket_id = 'event-content-images'
|
||||||
|
and auth.role() = 'authenticated'
|
||||||
|
and exists (
|
||||||
|
select 1 from public.profiles p
|
||||||
|
where p.id = auth.uid()
|
||||||
|
and p.is_admin
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Allow authenticated admins to delete content images
|
||||||
|
create policy "content_images_delete_admin" on storage.objects
|
||||||
|
for delete
|
||||||
|
using (
|
||||||
|
bucket_id = 'event-content-images'
|
||||||
|
and auth.role() = 'authenticated'
|
||||||
|
and exists (
|
||||||
|
select 1 from public.profiles p
|
||||||
|
where p.id = auth.uid()
|
||||||
|
and p.is_admin
|
||||||
|
)
|
||||||
|
);
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: [
|
||||||
|
'./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}',
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [require('@tailwindcss/typography')],
|
||||||
|
};
|
||||||