Compare commits
49 Commits
refactor/t
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a3fd0df2a | ||
|
|
8abcd3a979 | ||
|
|
ade34ac7ca | ||
|
|
3edae91e12 | ||
|
|
fcd11be506 | ||
|
670ecf3526
|
|||
|
e3e5e9eb69
|
|||
|
|
d086700d6d | ||
|
|
03bca19527 | ||
|
|
0239f86985 | ||
|
|
e518f63714 | ||
|
|
9306b42098 | ||
|
|
a2277a0c8b | ||
|
|
02c8b25b94 | ||
|
|
7d4e5bf4d1 | ||
|
|
f48d457f5b | ||
|
ba05fd478f
|
|||
|
9399a653a3
|
|||
|
|
7332c0376b | ||
|
b8af5c8374
|
|||
|
|
bfb361c82a | ||
|
|
947bb35f93 | ||
|
8af787c61c
|
|||
|
|
ef157d4015 | ||
|
|
958e6f61a4 | ||
|
|
9708713794 | ||
|
|
764385d660 | ||
|
|
9bcd1788bf | ||
|
|
37665bcb3a | ||
|
|
268392deed | ||
| e96aeb2cfc | |||
|
b22943968e
|
|||
|
|
f9812b3391 | ||
|
95c38c6f9f
|
|||
|
2ef9a63027
|
|||
|
|
602bf07d02 | ||
|
|
f4517ef467 | ||
|
|
b8f31aef5b | ||
|
|
d767bc4fad | ||
|
|
7f305287f0 | ||
|
|
2219f7a3b9 | ||
|
ce6c391d81
|
|||
|
|
b737c67377 | ||
|
|
93b3bf23be | ||
|
|
ee190d90db | ||
|
|
be04f2d869 | ||
|
|
ba0ae11abd | ||
|
|
e4a5641eeb | ||
|
|
8be06bba8b |
@@ -12,6 +12,6 @@
|
|||||||
%sveltekit.head%
|
%sveltekit.head%
|
||||||
</head>
|
</head>
|
||||||
<body data-sveltekit-preload-data="hover">
|
<body data-sveltekit-preload-data="hover">
|
||||||
<div style="display: contents">%sveltekit.body%</div>
|
<div style="display: contents; background: inherit">%sveltekit.body%</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<svg width="38" height="38" viewBox="0 0 38 38" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="38" height="38" viewBox="0 0 38 38" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<g clip-path="url(#clip0_117_282)">
|
<g clip-path="url(#clip0_117_282)">
|
||||||
<path d="M36.4168 30.0833C36.4168 30.9232 36.0832 31.7286 35.4893 32.3225C34.8955 32.9164 34.09 33.25 33.2502 33.25H4.75016C3.91031 33.25 3.10486 32.9164 2.51099 32.3225C1.91713 31.7286 1.5835 30.9232 1.5835 30.0833V12.6667C1.5835 11.8268 1.91713 11.0214 2.51099 10.4275C3.10486 9.83363 3.91031 9.5 4.75016 9.5H11.0835L14.2502 4.75H23.7502L26.9168 9.5H33.2502C34.09 9.5 34.8955 9.83363 35.4893 10.4275C36.0832 11.0214 36.4168 11.8268 36.4168 12.6667V30.0833Z" stroke="#49BD85" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
<path d="M36.4168 30.0833C36.4168 30.9232 36.0832 31.7286 35.4893 32.3225C34.8955 32.9164 34.09 33.25 33.2502 33.25H4.75016C3.91031 33.25 3.10486 32.9164 2.51099 32.3225C1.91713 31.7286 1.5835 30.9232 1.5835 30.0833V12.6667C1.5835 11.8268 1.91713 11.0214 2.51099 10.4275C3.10486 9.83363 3.91031 9.5 4.75016 9.5H11.0835L14.2502 4.75H23.7502L26.9168 9.5H33.2502C34.09 9.5 34.8955 9.83363 35.4893 10.4275C36.0832 11.0214 36.4168 11.8268 36.4168 12.6667V30.0833Z" stroke="#189f5e" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
<path d="M19.0002 26.9167C22.498 26.9167 25.3335 24.0811 25.3335 20.5833C25.3335 17.0855 22.498 14.25 19.0002 14.25C15.5024 14.25 12.6668 17.0855 12.6668 20.5833C12.6668 24.0811 15.5024 26.9167 19.0002 26.9167Z" stroke="#49BD85" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
<path d="M19.0002 26.9167C22.498 26.9167 25.3335 24.0811 25.3335 20.5833C25.3335 17.0855 22.498 14.25 19.0002 14.25C15.5024 14.25 12.6668 17.0855 12.6668 20.5833C12.6668 24.0811 15.5024 26.9167 19.0002 26.9167Z" stroke="#189f5e" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
</g>
|
</g>
|
||||||
<defs>
|
<defs>
|
||||||
<clipPath id="clip0_117_282">
|
<clipPath id="clip0_117_282">
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
3
src/lib/assets/heart.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="131" height="113" viewBox="0 0 131 113" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M0 38.2831C0 70.0945 26.2938 87.0466 45.5413 102.22C52.3333 107.574 58.875 112.615 65.4167 112.615C71.9583 112.615 78.5 107.574 85.2922 102.22C104.54 87.0466 130.833 70.0945 130.833 38.2831C130.833 6.47134 94.8529 -16.0889 65.4167 14.4945C35.9802 -16.0889 0 6.47134 0 38.2831Z" fill="#FF6060"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 411 B |
@@ -1,8 +1,8 @@
|
|||||||
<svg width="279" height="279" viewBox="0 0 279 279" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="279" height="279" viewBox="0 0 279 279" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<g clip-path="url(#clip0_194_118)">
|
<g clip-path="url(#clip0_194_118)">
|
||||||
<path d="M-6 20H216.872L269 70H-6V20Z" fill="#53988B"/>
|
<path d="M-6 20H216.872L269 70H-6V20Z" fill="#177665"/>
|
||||||
<path d="M-19 -12H84.9818L114 23H-19V-12Z" fill="#53988B"/>
|
<path d="M-19 -12H84.9818L114 23H-19V-12Z" fill="#177665"/>
|
||||||
<path d="M249.467 33L281 63V283L-9 280V33H249.467Z" fill="#49BD85"/>
|
<path d="M249.467 33L281 63V283L-9 280V33H249.467Z" fill="#189f5e"/>
|
||||||
<path d="M35.0419 177.732C34.843 175.727 33.9896 174.169 32.4815 173.059C30.9735 171.949 28.9268 171.393 26.3416 171.393C24.585 171.393 23.1018 171.642 21.892 172.139C20.6823 172.62 19.7543 173.291 19.108 174.153C18.4782 175.014 18.1634 175.992 18.1634 177.086C18.1302 177.997 18.3208 178.793 18.7351 179.472C19.166 180.152 19.7543 180.74 20.5 181.237C21.2457 181.718 22.1075 182.14 23.0852 182.505C24.063 182.853 25.107 183.151 26.2173 183.4L30.7912 184.494C33.0118 184.991 35.0502 185.654 36.9062 186.482C38.7623 187.311 40.3698 188.33 41.7287 189.54C43.0876 190.75 44.1399 192.175 44.8857 193.815C45.648 195.456 46.0374 197.337 46.054 199.458C46.0374 202.574 45.242 205.275 43.6676 207.562C42.1099 209.832 39.8561 211.597 36.9062 212.857C33.973 214.099 30.4349 214.721 26.2919 214.721C22.1821 214.721 18.6025 214.091 15.5533 212.832C12.5206 211.572 10.1508 209.708 8.44389 207.239C6.75355 204.753 5.86695 201.679 5.78409 198.016H16.1996C16.3156 199.723 16.8045 201.148 17.6662 202.292C18.5445 203.419 19.7128 204.272 21.1712 204.852C22.6461 205.416 24.3116 205.697 26.1676 205.697C27.9905 205.697 29.5732 205.432 30.9155 204.902C32.2744 204.372 33.3267 203.634 34.0724 202.69C34.8182 201.745 35.1911 200.66 35.1911 199.433C35.1911 198.29 34.8513 197.329 34.1719 196.55C33.509 195.771 32.5313 195.108 31.2386 194.561C29.9626 194.014 28.3965 193.517 26.5405 193.07L20.9972 191.678C16.705 190.634 13.3161 189.001 10.8303 186.781C8.34446 184.56 7.10985 181.569 7.12642 177.807C7.10985 174.724 7.93016 172.031 9.58736 169.728C11.2611 167.424 13.5563 165.626 16.473 164.334C19.3897 163.041 22.7041 162.395 26.4162 162.395C30.1946 162.395 33.4924 163.041 36.3097 164.334C39.1435 165.626 41.3475 167.424 42.9219 169.728C44.4962 172.031 45.3082 174.7 45.358 177.732H35.0419ZM51.3549 171.965V163.091H93.166V171.965H77.5801V214H66.9409V171.965H51.3549ZM131.497 163.091H142.26V196.152C142.26 199.864 141.374 203.112 139.601 205.896C137.844 208.68 135.383 210.851 132.218 212.409C129.053 213.95 125.365 214.721 121.156 214.721C116.93 214.721 113.235 213.95 110.069 212.409C106.904 210.851 104.443 208.68 102.687 205.896C100.93 203.112 100.052 199.864 100.052 196.152V163.091H110.815V195.232C110.815 197.171 111.238 198.895 112.083 200.403C112.945 201.911 114.154 203.096 115.712 203.957C117.27 204.819 119.085 205.25 121.156 205.25C123.244 205.25 125.059 204.819 126.6 203.957C128.158 203.096 129.359 201.911 130.204 200.403C131.066 198.895 131.497 197.171 131.497 195.232V163.091ZM169.163 214H151.116V163.091H169.312C174.433 163.091 178.841 164.11 182.537 166.148C186.232 168.17 189.074 171.079 191.063 174.874C193.068 178.669 194.071 183.209 194.071 188.496C194.071 193.799 193.068 198.356 191.063 202.168C189.074 205.979 186.216 208.904 182.487 210.942C178.775 212.981 174.333 214 169.163 214ZM161.88 204.778H168.716C171.897 204.778 174.574 204.214 176.745 203.087C178.932 201.944 180.573 200.179 181.667 197.793C182.777 195.39 183.332 192.291 183.332 188.496C183.332 184.734 182.777 181.66 181.667 179.273C180.573 176.887 178.94 175.13 176.77 174.004C174.599 172.877 171.922 172.313 168.74 172.313H161.88V204.778ZM195.985 163.091H208.041L219.65 185.016H220.147L231.755 163.091H243.811L225.243 196.003V214H214.554V196.003L195.985 163.091Z" fill="white"/>
|
<path d="M35.0419 177.732C34.843 175.727 33.9896 174.169 32.4815 173.059C30.9735 171.949 28.9268 171.393 26.3416 171.393C24.585 171.393 23.1018 171.642 21.892 172.139C20.6823 172.62 19.7543 173.291 19.108 174.153C18.4782 175.014 18.1634 175.992 18.1634 177.086C18.1302 177.997 18.3208 178.793 18.7351 179.472C19.166 180.152 19.7543 180.74 20.5 181.237C21.2457 181.718 22.1075 182.14 23.0852 182.505C24.063 182.853 25.107 183.151 26.2173 183.4L30.7912 184.494C33.0118 184.991 35.0502 185.654 36.9062 186.482C38.7623 187.311 40.3698 188.33 41.7287 189.54C43.0876 190.75 44.1399 192.175 44.8857 193.815C45.648 195.456 46.0374 197.337 46.054 199.458C46.0374 202.574 45.242 205.275 43.6676 207.562C42.1099 209.832 39.8561 211.597 36.9062 212.857C33.973 214.099 30.4349 214.721 26.2919 214.721C22.1821 214.721 18.6025 214.091 15.5533 212.832C12.5206 211.572 10.1508 209.708 8.44389 207.239C6.75355 204.753 5.86695 201.679 5.78409 198.016H16.1996C16.3156 199.723 16.8045 201.148 17.6662 202.292C18.5445 203.419 19.7128 204.272 21.1712 204.852C22.6461 205.416 24.3116 205.697 26.1676 205.697C27.9905 205.697 29.5732 205.432 30.9155 204.902C32.2744 204.372 33.3267 203.634 34.0724 202.69C34.8182 201.745 35.1911 200.66 35.1911 199.433C35.1911 198.29 34.8513 197.329 34.1719 196.55C33.509 195.771 32.5313 195.108 31.2386 194.561C29.9626 194.014 28.3965 193.517 26.5405 193.07L20.9972 191.678C16.705 190.634 13.3161 189.001 10.8303 186.781C8.34446 184.56 7.10985 181.569 7.12642 177.807C7.10985 174.724 7.93016 172.031 9.58736 169.728C11.2611 167.424 13.5563 165.626 16.473 164.334C19.3897 163.041 22.7041 162.395 26.4162 162.395C30.1946 162.395 33.4924 163.041 36.3097 164.334C39.1435 165.626 41.3475 167.424 42.9219 169.728C44.4962 172.031 45.3082 174.7 45.358 177.732H35.0419ZM51.3549 171.965V163.091H93.166V171.965H77.5801V214H66.9409V171.965H51.3549ZM131.497 163.091H142.26V196.152C142.26 199.864 141.374 203.112 139.601 205.896C137.844 208.68 135.383 210.851 132.218 212.409C129.053 213.95 125.365 214.721 121.156 214.721C116.93 214.721 113.235 213.95 110.069 212.409C106.904 210.851 104.443 208.68 102.687 205.896C100.93 203.112 100.052 199.864 100.052 196.152V163.091H110.815V195.232C110.815 197.171 111.238 198.895 112.083 200.403C112.945 201.911 114.154 203.096 115.712 203.957C117.27 204.819 119.085 205.25 121.156 205.25C123.244 205.25 125.059 204.819 126.6 203.957C128.158 203.096 129.359 201.911 130.204 200.403C131.066 198.895 131.497 197.171 131.497 195.232V163.091ZM169.163 214H151.116V163.091H169.312C174.433 163.091 178.841 164.11 182.537 166.148C186.232 168.17 189.074 171.079 191.063 174.874C193.068 178.669 194.071 183.209 194.071 188.496C194.071 193.799 193.068 198.356 191.063 202.168C189.074 205.979 186.216 208.904 182.487 210.942C178.775 212.981 174.333 214 169.163 214ZM161.88 204.778H168.716C171.897 204.778 174.574 204.214 176.745 203.087C178.932 201.944 180.573 200.179 181.667 197.793C182.777 195.39 183.332 192.291 183.332 188.496C183.332 184.734 182.777 181.66 181.667 179.273C180.573 176.887 178.94 175.13 176.77 174.004C174.599 172.877 171.922 172.313 168.74 172.313H161.88V204.778ZM195.985 163.091H208.041L219.65 185.016H220.147L231.755 163.091H243.811L225.243 196.003V214H214.554V196.003L195.985 163.091Z" fill="white"/>
|
||||||
<path d="M66.0419 233.732C65.843 231.727 64.9896 230.169 63.4815 229.059C61.9735 227.949 59.9268 227.393 57.3416 227.393C55.585 227.393 54.1018 227.642 52.892 228.139C51.6823 228.62 50.7543 229.291 50.108 230.153C49.4782 231.014 49.1634 231.992 49.1634 233.086C49.1302 233.997 49.3208 234.793 49.7351 235.472C50.166 236.152 50.7543 236.74 51.5 237.237C52.2457 237.718 53.1075 238.14 54.0852 238.505C55.063 238.853 56.107 239.151 57.2173 239.4L61.7912 240.494C64.0118 240.991 66.0502 241.654 67.9062 242.482C69.7623 243.311 71.3698 244.33 72.7287 245.54C74.0876 246.75 75.1399 248.175 75.8857 249.815C76.648 251.456 77.0374 253.337 77.054 255.458C77.0374 258.574 76.242 261.275 74.6676 263.562C73.1099 265.832 70.8561 267.597 67.9062 268.857C64.973 270.099 61.4349 270.721 57.2919 270.721C53.1821 270.721 49.6025 270.091 46.5533 268.832C43.5206 267.572 41.1508 265.708 39.4439 263.239C37.7536 260.753 36.867 257.679 36.7841 254.016H47.1996C47.3156 255.723 47.8045 257.148 48.6662 258.292C49.5445 259.419 50.7128 260.272 52.1712 260.852C53.6461 261.416 55.3116 261.697 57.1676 261.697C58.9905 261.697 60.5732 261.432 61.9155 260.902C63.2744 260.372 64.3267 259.634 65.0724 258.69C65.8182 257.745 66.1911 256.66 66.1911 255.433C66.1911 254.29 65.8513 253.329 65.1719 252.55C64.509 251.771 63.5313 251.108 62.2386 250.561C60.9626 250.014 59.3965 249.517 57.5405 249.07L51.9972 247.678C47.705 246.634 44.3161 245.001 41.8303 242.781C39.3445 240.56 38.1098 237.569 38.1264 233.807C38.1098 230.724 38.9302 228.031 40.5874 225.728C42.2611 223.424 44.5563 221.626 47.473 220.334C50.3897 219.041 53.7041 218.395 57.4162 218.395C61.1946 218.395 64.4924 219.041 67.3097 220.334C70.1435 221.626 72.3475 223.424 73.9219 225.728C75.4962 228.031 76.3082 230.7 76.358 233.732H66.0419ZM91.9066 270H80.3725L97.9471 219.091H111.818L129.368 270H117.833L105.081 230.724H104.684L91.9066 270ZM91.1857 249.989H118.43V258.391H91.1857V249.989ZM138.335 219.091L150.64 257.77H151.112L163.442 219.091H175.373L157.824 270H143.953L126.378 219.091H138.335ZM181.501 270V219.091H215.805V227.965H192.264V240.096H214.04V248.97H192.264V261.126H215.904V270H181.501ZM224.362 270V219.091H244.447C248.292 219.091 251.573 219.779 254.291 221.154C257.026 222.513 259.105 224.444 260.531 226.946C261.972 229.432 262.693 232.357 262.693 235.721C262.693 239.102 261.964 242.01 260.506 244.446C259.047 246.866 256.934 248.722 254.167 250.014C251.416 251.307 248.085 251.953 244.174 251.953H230.726V243.303H242.434C244.489 243.303 246.196 243.021 247.555 242.457C248.914 241.894 249.924 241.049 250.587 239.922C251.267 238.795 251.607 237.395 251.607 235.721C251.607 234.031 251.267 232.605 250.587 231.445C249.924 230.285 248.905 229.407 247.53 228.81C246.171 228.197 244.456 227.891 242.384 227.891H235.126V270H224.362ZM251.855 246.832L264.508 270H252.626L240.246 246.832H251.855Z" fill="white"/>
|
<path d="M66.0419 233.732C65.843 231.727 64.9896 230.169 63.4815 229.059C61.9735 227.949 59.9268 227.393 57.3416 227.393C55.585 227.393 54.1018 227.642 52.892 228.139C51.6823 228.62 50.7543 229.291 50.108 230.153C49.4782 231.014 49.1634 231.992 49.1634 233.086C49.1302 233.997 49.3208 234.793 49.7351 235.472C50.166 236.152 50.7543 236.74 51.5 237.237C52.2457 237.718 53.1075 238.14 54.0852 238.505C55.063 238.853 56.107 239.151 57.2173 239.4L61.7912 240.494C64.0118 240.991 66.0502 241.654 67.9062 242.482C69.7623 243.311 71.3698 244.33 72.7287 245.54C74.0876 246.75 75.1399 248.175 75.8857 249.815C76.648 251.456 77.0374 253.337 77.054 255.458C77.0374 258.574 76.242 261.275 74.6676 263.562C73.1099 265.832 70.8561 267.597 67.9062 268.857C64.973 270.099 61.4349 270.721 57.2919 270.721C53.1821 270.721 49.6025 270.091 46.5533 268.832C43.5206 267.572 41.1508 265.708 39.4439 263.239C37.7536 260.753 36.867 257.679 36.7841 254.016H47.1996C47.3156 255.723 47.8045 257.148 48.6662 258.292C49.5445 259.419 50.7128 260.272 52.1712 260.852C53.6461 261.416 55.3116 261.697 57.1676 261.697C58.9905 261.697 60.5732 261.432 61.9155 260.902C63.2744 260.372 64.3267 259.634 65.0724 258.69C65.8182 257.745 66.1911 256.66 66.1911 255.433C66.1911 254.29 65.8513 253.329 65.1719 252.55C64.509 251.771 63.5313 251.108 62.2386 250.561C60.9626 250.014 59.3965 249.517 57.5405 249.07L51.9972 247.678C47.705 246.634 44.3161 245.001 41.8303 242.781C39.3445 240.56 38.1098 237.569 38.1264 233.807C38.1098 230.724 38.9302 228.031 40.5874 225.728C42.2611 223.424 44.5563 221.626 47.473 220.334C50.3897 219.041 53.7041 218.395 57.4162 218.395C61.1946 218.395 64.4924 219.041 67.3097 220.334C70.1435 221.626 72.3475 223.424 73.9219 225.728C75.4962 228.031 76.3082 230.7 76.358 233.732H66.0419ZM91.9066 270H80.3725L97.9471 219.091H111.818L129.368 270H117.833L105.081 230.724H104.684L91.9066 270ZM91.1857 249.989H118.43V258.391H91.1857V249.989ZM138.335 219.091L150.64 257.77H151.112L163.442 219.091H175.373L157.824 270H143.953L126.378 219.091H138.335ZM181.501 270V219.091H215.805V227.965H192.264V240.096H214.04V248.97H192.264V261.126H215.904V270H181.501ZM224.362 270V219.091H244.447C248.292 219.091 251.573 219.779 254.291 221.154C257.026 222.513 259.105 224.444 260.531 226.946C261.972 229.432 262.693 232.357 262.693 235.721C262.693 239.102 261.964 242.01 260.506 244.446C259.047 246.866 256.934 248.722 254.167 250.014C251.416 251.307 248.085 251.953 244.174 251.953H230.726V243.303H242.434C244.489 243.303 246.196 243.021 247.555 242.457C248.914 241.894 249.924 241.049 250.587 239.922C251.267 238.795 251.607 237.395 251.607 235.721C251.607 234.031 251.267 232.605 250.587 231.445C249.924 230.285 248.905 229.407 247.53 228.81C246.171 228.197 244.456 227.891 242.384 227.891H235.126V270H224.362ZM251.855 246.832L264.508 270H252.626L240.246 246.832H251.855Z" fill="white"/>
|
||||||
<path d="M262.946 92.0066C257.24 97.7121 249.502 100.917 241.433 100.917C233.365 100.917 225.626 97.7121 219.921 92.0066C214.216 86.3012 211.01 78.563 211.01 70.4943C211.01 62.4256 214.216 54.6873 219.921 48.9819L241.433 70.4943L262.946 92.0066Z" fill="white"/>
|
<path d="M262.946 92.0066C257.24 97.7121 249.502 100.917 241.433 100.917C233.365 100.917 225.626 97.7121 219.921 92.0066C214.216 86.3012 211.01 78.563 211.01 70.4943C211.01 62.4256 214.216 54.6873 219.921 48.9819L241.433 70.4943L262.946 92.0066Z" fill="white"/>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 6.6 KiB |
3
src/lib/assets/search.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M42 42L33.3 33.3M38 22C38 30.8366 30.8366 38 22 38C13.1634 38 6 30.8366 6 22C6 13.1634 13.1634 6 22 6C30.8366 6 38 13.1634 38 22Z" stroke="white" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 324 B |
4
src/lib/assets/un_heart.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg width="138" height="122" viewBox="0 0 138 122" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M0.0834961 46.7718C0.0834961 78.5833 26.3773 95.5354 45.6248 110.709C52.4168 116.063 58.9585 121.104 65.5002 121.104C72.0418 121.104 78.5835 116.063 85.3757 110.709C104.623 95.5354 130.917 78.5833 130.917 46.7718C130.917 14.9601 94.9364 -7.60011 65.5002 22.9833C36.0637 -7.60011 0.0834961 14.9601 0.0834961 46.7718Z" fill="#2E4653"/>
|
||||||
|
<path d="M21 103L125 13" stroke="white" stroke-width="25" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 533 B |
@@ -5,7 +5,7 @@
|
|||||||
onclick?: (event: MouseEvent) => void;
|
onclick?: (event: MouseEvent) => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
type?: "button" | "submit" | "reset";
|
type?: "button" | "submit" | "reset";
|
||||||
style?: "normal" | "red";
|
style?: "normal" | "red" | "invisible";
|
||||||
formaction?: string;
|
formaction?: string;
|
||||||
children?: Snippet;
|
children?: Snippet;
|
||||||
}
|
}
|
||||||
@@ -33,18 +33,21 @@
|
|||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
box-shadow: 0rem 0rem 0.5rem #182125;
|
box-shadow: 0rem 0rem 0.5rem #182125;
|
||||||
color: #eaffeb;
|
color: #ffffff;
|
||||||
border: none;
|
border: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
.normal {
|
.normal {
|
||||||
background: linear-gradient(-83deg, #3fb095, #49bd85);
|
background: linear-gradient(-83deg, rgb(1, 163, 117), #189f5e);
|
||||||
}
|
}
|
||||||
.red {
|
.red {
|
||||||
background-color: #bd4949;
|
background-color: #bd4949;
|
||||||
}
|
}
|
||||||
|
.invisible {
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
.button:focus {
|
.button:focus {
|
||||||
outline: 2px solid #007bff;
|
outline: 2px solid #007bff;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
"No Outlets": "compulsoryTagRed",
|
"No Outlets": "compulsoryTagRed",
|
||||||
"Some Outlets": "compulsoryTagYellow",
|
"Some Outlets": "compulsoryTagYellow",
|
||||||
"Good WiFi": "compulsoryTagGreen",
|
"Good WiFi": "compulsoryTagGreen",
|
||||||
"Bad WiFi": "compulsoryTagRed",
|
"Bad/No WiFi": "compulsoryTagRed",
|
||||||
"Moderate WiFi": "compulsoryTagYellow",
|
"Moderate WiFi": "compulsoryTagYellow",
|
||||||
"No WiFi": "compulsoryTagRed"
|
"No WiFi": "compulsoryTagRed"
|
||||||
};
|
};
|
||||||
|
|||||||
55
src/lib/components/Favourite.svelte
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import heart from "../assets/heart.svg";
|
||||||
|
import un_heart from "../assets/un_heart.svg";
|
||||||
|
interface Props {
|
||||||
|
isFavourite: boolean;
|
||||||
|
onToggleFavourite: () => void;
|
||||||
|
imgSize?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { isFavourite, onToggleFavourite, imgSize }: Props = $props();
|
||||||
|
|
||||||
|
function handleClick(event: MouseEvent) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
onToggleFavourite();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="fav-button"
|
||||||
|
style="--imgSize: {imgSize || 20}%"
|
||||||
|
onclick={handleClick}
|
||||||
|
aria-label="Toggle favourite"
|
||||||
|
>
|
||||||
|
{#if isFavourite}
|
||||||
|
<img class="favImg shadow" src={heart} alt="heart" />
|
||||||
|
{:else}
|
||||||
|
<img class="unfav shadow" src={un_heart} alt="unheart" />
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.fav-button {
|
||||||
|
background: none;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favImg {
|
||||||
|
transform: scale(var(--imgSize));
|
||||||
|
}
|
||||||
|
.shadow {
|
||||||
|
filter: drop-shadow(5px 5px 5px rgba(0, 0, 0, 0.3));
|
||||||
|
}
|
||||||
|
.unfav {
|
||||||
|
transform: scale(var(--imgSize)) translate(0.1rem);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -26,12 +26,18 @@
|
|||||||
const { error: feedbackUpload } = await supabase
|
const { error: feedbackUpload } = await supabase
|
||||||
.from("study_spaces")
|
.from("study_spaces")
|
||||||
.update({
|
.update({
|
||||||
|
directions: newStudySpaceData.directions,
|
||||||
volume: newStudySpaceData.volume,
|
volume: newStudySpaceData.volume,
|
||||||
wifi: newStudySpaceData.wifi,
|
wifi: newStudySpaceData.wifi,
|
||||||
power: newStudySpaceData.power,
|
power: newStudySpaceData.power,
|
||||||
tags: newStudySpaceData.tags
|
tags: newStudySpaceData.tags
|
||||||
})
|
})
|
||||||
.eq("id", newStudySpaceData.id ?? "");
|
.eq("id", newStudySpaceData.id ?? "");
|
||||||
|
await supabase.channel("study_space_updates").send({
|
||||||
|
type: "broadcast",
|
||||||
|
event: "study_space_updated",
|
||||||
|
payload: { id: newStudySpaceData.id }
|
||||||
|
});
|
||||||
invalidate("db:study_spaces");
|
invalidate("db:study_spaces");
|
||||||
if (feedbackUpload) return alert(`Error submitting feedback: ${feedbackUpload.message}`);
|
if (feedbackUpload) return alert(`Error submitting feedback: ${feedbackUpload.message}`);
|
||||||
else alert("Feedback submitted successfully!");
|
else alert("Feedback submitted successfully!");
|
||||||
@@ -74,7 +80,7 @@
|
|||||||
}}
|
}}
|
||||||
class="feedbackContainer"
|
class="feedbackContainer"
|
||||||
>
|
>
|
||||||
<h1 class="submitHeader">Submit Feedback</h1>
|
<h1 class="submitHeader">Update Tags</h1>
|
||||||
|
|
||||||
<div class="compulsoryTags">
|
<div class="compulsoryTags">
|
||||||
<div class="compulsoryContainer">
|
<div class="compulsoryContainer">
|
||||||
@@ -206,7 +212,7 @@
|
|||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
border: none;
|
border: none;
|
||||||
background-color: #49bd85;
|
background-color: #189f5e;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
background: linear-gradient(-77deg, #2e4653, #223a37);
|
background: linear-gradient(-77deg, #2e4653, #223a37);
|
||||||
box-shadow: 0rem 0rem 0.5rem #182125;
|
box-shadow: 0rem 0rem 0.5rem #182125;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
overflow: hidden;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,5 +45,6 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row-reverse;
|
flex-direction: row-reverse;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -18,8 +18,8 @@
|
|||||||
// Compute the display string for opening times
|
// Compute the display string for opening times
|
||||||
let openingDisplay = $state("");
|
let openingDisplay = $state("");
|
||||||
if (todayHours) {
|
if (todayHours) {
|
||||||
openingDisplay = todayHours.is_24_7
|
openingDisplay = todayHours.open_today_status
|
||||||
? "Open 24/7"
|
? "Open All Day"
|
||||||
: `${formatTime(todayHours.opens_at)} - ${formatTime(todayHours.closes_at)}`;
|
: `${formatTime(todayHours.opens_at)} - ${formatTime(todayHours.closes_at)}`;
|
||||||
} else {
|
} else {
|
||||||
openingDisplay = "Closed";
|
openingDisplay = "Closed";
|
||||||
|
|||||||
@@ -29,6 +29,12 @@
|
|||||||
.select()
|
.select()
|
||||||
.single();
|
.single();
|
||||||
|
|
||||||
|
await supabase.channel("report_updates").send({
|
||||||
|
type: "broadcast",
|
||||||
|
event: "reports_updated",
|
||||||
|
payload: { study_space_id: studySpaceId }
|
||||||
|
});
|
||||||
|
|
||||||
if (reportUploadError)
|
if (reportUploadError)
|
||||||
return alert(`Error submitting report: ${reportUploadError.message}`);
|
return alert(`Error submitting report: ${reportUploadError.message}`);
|
||||||
else alert("Report submitted successfully!");
|
else alert("Report submitted successfully!");
|
||||||
@@ -123,7 +129,7 @@
|
|||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
border: none;
|
border: none;
|
||||||
background-color: #49bd85;
|
background-color: #189f5e;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|||||||
@@ -1,32 +1,47 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import CompulsoryTags from "./CompulsoryTags.svelte";
|
import CompulsoryTags from "./CompulsoryTags.svelte";
|
||||||
import OpeningTimes from "./OpeningTimes.svelte";
|
import Favourite from "./Favourite.svelte";
|
||||||
import type { Table } from "$lib";
|
import type { Table } from "$lib";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
space: Table<"study_spaces">;
|
space: Table<"study_spaces">;
|
||||||
hours: Table<"study_space_hours">[];
|
|
||||||
alt: string;
|
alt: string;
|
||||||
imgSrc: string;
|
imgSrc: string;
|
||||||
href?: string;
|
href?: string;
|
||||||
|
isFavourite: boolean;
|
||||||
|
isAvailable?: boolean;
|
||||||
|
onToggleFavourite?: () => void;
|
||||||
|
footer?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { space, hours, alt, imgSrc, href }: Props = $props();
|
const { space, alt, imgSrc, href, isFavourite, onToggleFavourite, isAvailable, footer }: Props =
|
||||||
|
$props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<a class="card" {href}>
|
<a class="card {isAvailable ? 'green' : 'grey'}" {href}>
|
||||||
|
<!-- <img src={imgSrc} {alt} /> -->
|
||||||
|
<div class="image-container">
|
||||||
<img src={imgSrc} {alt} />
|
<img src={imgSrc} {alt} />
|
||||||
|
{#if onToggleFavourite}
|
||||||
|
<div class="fav-button">
|
||||||
|
<Favourite {isFavourite} {onToggleFavourite} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
<div class="description">
|
<div class="description">
|
||||||
<h1>{space.location}</h1>
|
<h1>{space.location}</h1>
|
||||||
<div class="compulsoryContainer"><CompulsoryTags {space} /></div>
|
<div class="compulsoryContainer"><CompulsoryTags {space} /></div>
|
||||||
{#if space.tags.length > 0}
|
{#if space.tags.length > 0}
|
||||||
<div class="tagContainer">
|
<div class="tagContainer">
|
||||||
{#each space.tags as tag (tag)}
|
{#each space.tags as tag (tag)}
|
||||||
<span class="tag">{tag}</span>
|
<span class="tag {isAvailable ? 'tagGreen' : 'tagGrey'}">{tag}</span>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="openingTimesContainer"><OpeningTimes {hours} /></div>
|
<div class="spacer"></div>
|
||||||
|
{#if footer}
|
||||||
|
<div class="footer">{footer}</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
@@ -34,15 +49,26 @@
|
|||||||
.card {
|
.card {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background-color: #49bd85;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 20rem;
|
max-width: 20rem;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
.green {
|
||||||
|
background-color: #189f5e;
|
||||||
|
}
|
||||||
|
.grey {
|
||||||
|
background-color: #2e4653;
|
||||||
|
}
|
||||||
|
.spacer {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
.description {
|
.description {
|
||||||
padding: 0.5rem;
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 0.4rem;
|
||||||
color: #edebe9;
|
color: #edebe9;
|
||||||
}
|
}
|
||||||
img {
|
img {
|
||||||
@@ -54,6 +80,7 @@
|
|||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
|
font-size: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tagContainer {
|
.tagContainer {
|
||||||
@@ -71,17 +98,50 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
background-color: #2e4653;
|
|
||||||
color: #eaffeb;
|
color: #eaffeb;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 0.2rem 0.6rem;
|
padding: 0.2rem 0.6rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tagGreen {
|
||||||
|
background-color: #2e4653;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tagGrey {
|
||||||
|
background-color: #189f5e;
|
||||||
|
}
|
||||||
|
|
||||||
.compulsoryContainer {
|
.compulsoryContainer {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(3.75rem, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(3.7rem, 1fr));
|
||||||
gap: 0.3rem;
|
gap: 0.3rem;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.image-container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.image-container .fav-button {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
background: #189f5e;
|
||||||
|
border-radius: 0 0 0 0.5rem;
|
||||||
|
z-index: 1;
|
||||||
|
width: 2.75rem;
|
||||||
|
height: 2.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
width: 100%;
|
||||||
|
color: #2e4653;
|
||||||
|
background-color: #eaffeb;
|
||||||
|
align-self: flex-end;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 0.1rem;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-top: 0.4rem;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -85,7 +85,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
color: #49bd85;
|
color: #189f5e;
|
||||||
}
|
}
|
||||||
.preview {
|
.preview {
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
|
|||||||
131
src/lib/components/inputs/OpeningTimesDay.svelte
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { daysOfWeek } from "$lib";
|
||||||
|
import CrossUrl from "../../assets/cross.svg";
|
||||||
|
interface Props {
|
||||||
|
index: number;
|
||||||
|
openingValue: string;
|
||||||
|
closingValue: string;
|
||||||
|
openTodayStatus: boolean | null;
|
||||||
|
onHide?: () => void;
|
||||||
|
day: number; //0-6 for Sunday-Saturday, 7 for all days, 8 for all other days
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
index,
|
||||||
|
openingValue = $bindable(),
|
||||||
|
closingValue = $bindable(),
|
||||||
|
openTodayStatus = $bindable(),
|
||||||
|
day = $bindable(),
|
||||||
|
onHide
|
||||||
|
}: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="opening-time-item">
|
||||||
|
{#if day <= 6}
|
||||||
|
<select bind:value={day} class="dayOfWeek">
|
||||||
|
{#each [0, 1, 2, 3, 4, 5, 6] as dayNum (dayNum)}
|
||||||
|
<option value={dayNum}>{daysOfWeek[dayNum]}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
{:else}
|
||||||
|
<label for={"opens-" + index} class="dayOfWeek">{daysOfWeek[day]}</label>
|
||||||
|
{/if}
|
||||||
|
<label for={"isAllDay-" + index}>
|
||||||
|
<input
|
||||||
|
id={"isAllDay-" + index}
|
||||||
|
class="checkbox"
|
||||||
|
type="checkbox"
|
||||||
|
bind:checked={
|
||||||
|
() => openTodayStatus === true, (v) => (openTodayStatus = v ? true : null)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
Open all day
|
||||||
|
</label>
|
||||||
|
<label for={"isclosed-" + index}>
|
||||||
|
<input
|
||||||
|
id={"isclosed-" + index}
|
||||||
|
class="checkbox"
|
||||||
|
type="checkbox"
|
||||||
|
bind:checked={
|
||||||
|
() => openTodayStatus === false, (v) => (openTodayStatus = v ? false : null)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
Closed
|
||||||
|
</label>
|
||||||
|
{#if onHide}
|
||||||
|
<button class="hideButton" onclick={onHide} type="button">
|
||||||
|
<img src={CrossUrl} alt="nah" />
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
{#if openTodayStatus === null}
|
||||||
|
<div class="timeRange">
|
||||||
|
<input id={"opens-" + index} type="time" bind:value={openingValue} />
|
||||||
|
<span class="to">to</span>
|
||||||
|
<input id={"closes-" + index} type="time" bind:value={closingValue} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.opening-time-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 0.25rem 1rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="time"] {
|
||||||
|
border: 2px solid #000000;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: none;
|
||||||
|
color: #000000;
|
||||||
|
padding: 0.25rem;
|
||||||
|
flex: 1;
|
||||||
|
filter: brightness(0) saturate(100%) invert(98%) sepia(8%) saturate(555%) hue-rotate(54deg)
|
||||||
|
brightness(100%) contrast(102%);
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="checkbox"] {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeRange {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dayOfWeek {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-weight: bold;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
background-color: #eaffeb;
|
||||||
|
color: #2e4653;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.2rem 0.4rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hideButton {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
margin: 0 0.5% 0 auto;
|
||||||
|
width: 5%;
|
||||||
|
padding: 0;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hideButton img {
|
||||||
|
width: 120%;
|
||||||
|
transform: scale(2);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
type?: "text" | "password" | "email" | "number";
|
type?: "text" | "password" | "email" | "number";
|
||||||
|
maxlength?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { inputElem = $bindable(), value = $bindable(), name, ...rest }: Props = $props();
|
let { inputElem = $bindable(), value = $bindable(), name, ...rest }: Props = $props();
|
||||||
|
|||||||
76
src/lib/database.d.ts
vendored
@@ -69,6 +69,47 @@ export type Database = {
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
study_space_hours: {
|
||||||
|
Row: {
|
||||||
|
closes_at: string
|
||||||
|
created_at: string | null
|
||||||
|
day_of_week: number
|
||||||
|
id: string
|
||||||
|
open_today_status: boolean | null
|
||||||
|
opens_at: string
|
||||||
|
study_space_id: string | null
|
||||||
|
updated_at: string | null
|
||||||
|
}
|
||||||
|
Insert: {
|
||||||
|
closes_at: string
|
||||||
|
created_at?: string | null
|
||||||
|
day_of_week: number
|
||||||
|
id?: string
|
||||||
|
open_today_status?: boolean | null
|
||||||
|
opens_at: string
|
||||||
|
study_space_id?: string | null
|
||||||
|
updated_at?: string | null
|
||||||
|
}
|
||||||
|
Update: {
|
||||||
|
closes_at?: string
|
||||||
|
created_at?: string | null
|
||||||
|
day_of_week?: number
|
||||||
|
id?: string
|
||||||
|
open_today_status?: boolean | null
|
||||||
|
opens_at?: string
|
||||||
|
study_space_id?: string | null
|
||||||
|
updated_at?: string | null
|
||||||
|
}
|
||||||
|
Relationships: [
|
||||||
|
{
|
||||||
|
foreignKeyName: "study_space_hours_study_space_id_fkey"
|
||||||
|
columns: ["study_space_id"]
|
||||||
|
isOneToOne: false
|
||||||
|
referencedRelation: "study_spaces"
|
||||||
|
referencedColumns: ["id"]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
study_space_images: {
|
study_space_images: {
|
||||||
Row: {
|
Row: {
|
||||||
created_at: string | null
|
created_at: string | null
|
||||||
@@ -104,6 +145,7 @@ export type Database = {
|
|||||||
building_location_old: string | null
|
building_location_old: string | null
|
||||||
created_at: string | null
|
created_at: string | null
|
||||||
description: string | null
|
description: string | null
|
||||||
|
directions: string
|
||||||
id: string
|
id: string
|
||||||
location: string | null
|
location: string | null
|
||||||
power: string
|
power: string
|
||||||
@@ -117,6 +159,7 @@ export type Database = {
|
|||||||
building_location_old?: string | null
|
building_location_old?: string | null
|
||||||
created_at?: string | null
|
created_at?: string | null
|
||||||
description?: string | null
|
description?: string | null
|
||||||
|
directions: string
|
||||||
id?: string
|
id?: string
|
||||||
location?: string | null
|
location?: string | null
|
||||||
power: string
|
power: string
|
||||||
@@ -130,6 +173,7 @@ export type Database = {
|
|||||||
building_location_old?: string | null
|
building_location_old?: string | null
|
||||||
created_at?: string | null
|
created_at?: string | null
|
||||||
description?: string | null
|
description?: string | null
|
||||||
|
directions?: string
|
||||||
id?: string
|
id?: string
|
||||||
location?: string | null
|
location?: string | null
|
||||||
power?: string
|
power?: string
|
||||||
@@ -161,45 +205,40 @@ export type Database = {
|
|||||||
}
|
}
|
||||||
Relationships: []
|
Relationships: []
|
||||||
}
|
}
|
||||||
study_space_hours: {
|
favourite_study_spaces: {
|
||||||
Row: {
|
Row: {
|
||||||
id: string
|
user_id: string
|
||||||
study_space_id: string
|
study_space_id: string
|
||||||
day_of_week: number
|
|
||||||
opens_at: string
|
|
||||||
closes_at: string
|
|
||||||
is_24_7: boolean
|
|
||||||
created_at: string | null
|
created_at: string | null
|
||||||
updated_at: string | null
|
updated_at: string | null
|
||||||
}
|
}
|
||||||
Insert: {
|
Insert: {
|
||||||
id?: string
|
user_id: string
|
||||||
study_space_id: string
|
study_space_id: string
|
||||||
day_of_week: number
|
|
||||||
opens_at: string
|
|
||||||
closes_at: string
|
|
||||||
is_24_7: boolean
|
|
||||||
created_at?: string | null
|
created_at?: string | null
|
||||||
updated_at?: string | null
|
updated_at?: string | null
|
||||||
}
|
}
|
||||||
Update: {
|
Update: {
|
||||||
id?: string
|
user_id?: string
|
||||||
study_space_id?: string
|
study_space_id?: string
|
||||||
day_of_week?: number
|
|
||||||
opens_at?: string
|
|
||||||
closes_at?: string
|
|
||||||
is_24_7?: boolean
|
|
||||||
created_at?: string | null
|
created_at?: string | null
|
||||||
updated_at?: string | null
|
updated_at?: string | null
|
||||||
}
|
}
|
||||||
Relationships: [
|
Relationships: [
|
||||||
{
|
{
|
||||||
foreignKeyName: "study_space_hours_study_space_id_fkey"
|
foreignKeyName: "favourite_study_spaces_user_id_fkey"
|
||||||
|
columns: ["user_id"]
|
||||||
|
isOneToOne: false
|
||||||
|
referencedRelation: "users"
|
||||||
|
referencedColumns: ["id"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
foreignKeyName: "favourite_study_spaces_study_space_id_fkey"
|
||||||
columns: ["study_space_id"]
|
columns: ["study_space_id"]
|
||||||
isOneToOne: false
|
isOneToOne: false
|
||||||
referencedRelation: "study_spaces"
|
referencedRelation: "study_spaces"
|
||||||
referencedColumns: ["id"]
|
referencedColumns: ["id"]
|
||||||
},
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -331,3 +370,4 @@ export const Constants = {
|
|||||||
Enums: {},
|
Enums: {},
|
||||||
},
|
},
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
|
|||||||
46
src/lib/filter.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
export interface SortFiler {
|
||||||
|
tags: string[];
|
||||||
|
/** Time strings of opening range. */
|
||||||
|
openAt: {
|
||||||
|
from: string;
|
||||||
|
to?: string;
|
||||||
|
};
|
||||||
|
nearby: {
|
||||||
|
lat: number;
|
||||||
|
lng: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function urlencodeSortFilter(filter: Partial<SortFiler>): string {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (filter.tags) {
|
||||||
|
filter.tags.forEach((tag) => params.append("tags", tag));
|
||||||
|
}
|
||||||
|
if (filter.openAt) {
|
||||||
|
params.set("open_from", filter.openAt.from);
|
||||||
|
if (filter.openAt.to) params.set("open_to", filter.openAt.to);
|
||||||
|
}
|
||||||
|
if (filter.nearby) {
|
||||||
|
params.set("nearby", `${filter.nearby.lat},${filter.nearby.lng}`);
|
||||||
|
}
|
||||||
|
return params.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function urldecodeSortFilter(query: string): Partial<SortFiler> {
|
||||||
|
const params = new URLSearchParams(query);
|
||||||
|
const filter: Partial<SortFiler> = {};
|
||||||
|
if (params.has("tags")) {
|
||||||
|
filter.tags = params.getAll("tags");
|
||||||
|
}
|
||||||
|
if (params.has("open_from")) {
|
||||||
|
filter.openAt = {
|
||||||
|
from: params.get("open_from")!,
|
||||||
|
to: params.get("open_to") ?? undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (params.has("nearby")) {
|
||||||
|
const [lat, lng] = params.get("nearby")!.split(",").map(Number);
|
||||||
|
filter.nearby = { lat, lng };
|
||||||
|
}
|
||||||
|
return filter;
|
||||||
|
}
|
||||||
@@ -18,7 +18,7 @@ export const availableStudySpaceTags = [
|
|||||||
"Air conditioned",
|
"Air conditioned",
|
||||||
"Cold",
|
"Cold",
|
||||||
"PCs",
|
"PCs",
|
||||||
"Cringe"
|
"Rodent-ridden"
|
||||||
];
|
];
|
||||||
|
|
||||||
export const volumeTags = ["Silent", "Some Noise", "Loud"];
|
export const volumeTags = ["Silent", "Some Noise", "Loud"];
|
||||||
@@ -57,5 +57,56 @@ export const daysOfWeek = [
|
|||||||
"Wednesday",
|
"Wednesday",
|
||||||
"Thursday",
|
"Thursday",
|
||||||
"Friday",
|
"Friday",
|
||||||
"Saturday"
|
"Saturday",
|
||||||
|
"All Days",
|
||||||
|
"All Other Days"
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Convert "HH:MM" or "HH:MM:SS" to minutes since midnight
|
||||||
|
export function timeToMins(timeStr: string): number {
|
||||||
|
const [h, m] = timeStr.slice(0, 5).split(":").map(Number);
|
||||||
|
return h * 60 + m;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function haversineDistance(
|
||||||
|
lat1Deg: number,
|
||||||
|
lng1Deg: number,
|
||||||
|
lat2Deg: number,
|
||||||
|
lng2Deg: number,
|
||||||
|
radius: number = 6371e3
|
||||||
|
): number {
|
||||||
|
const lat1 = lat1Deg * (Math.PI / 180);
|
||||||
|
const lat2 = lat2Deg * (Math.PI / 180);
|
||||||
|
const deltaLat = (lat2Deg - lat1Deg) * (Math.PI / 180);
|
||||||
|
const deltaLng = (lng2Deg - lng1Deg) * (Math.PI / 180);
|
||||||
|
const e1 =
|
||||||
|
Math.pow(Math.sin(deltaLat / 2), 2) +
|
||||||
|
Math.pow(Math.sin(deltaLng / 2), 2) * Math.cos(lat1) * Math.cos(lat2);
|
||||||
|
return radius * 2 * Math.asin(Math.sqrt(e1));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function collectTimings(
|
||||||
|
study_space_hours: Omit<
|
||||||
|
Table<"study_space_hours">,
|
||||||
|
"id" | "created_at" | "updated_at" | "study_space_id"
|
||||||
|
>[]
|
||||||
|
) {
|
||||||
|
// Collect all timing entries
|
||||||
|
const timingsPerDay: Record<
|
||||||
|
number,
|
||||||
|
Omit<Table<"study_space_hours">, "id" | "created_at" | "updated_at" | "study_space_id">[]
|
||||||
|
> = {
|
||||||
|
0: [],
|
||||||
|
1: [],
|
||||||
|
2: [],
|
||||||
|
3: [],
|
||||||
|
4: [],
|
||||||
|
5: [],
|
||||||
|
6: []
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const entry of study_space_hours) {
|
||||||
|
timingsPerDay[entry.day_of_week].push(entry);
|
||||||
|
}
|
||||||
|
return timingsPerDay;
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
import { invalidate } from "$app/navigation";
|
import { invalidate } from "$app/navigation";
|
||||||
|
|
||||||
let { data, children } = $props();
|
let { data, children } = $props();
|
||||||
let { session, supabase } = $derived(data);
|
let { session, supabase, route } = $derived(data);
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
posthog.init("phc_hTnel2Q8GKo0TgIBnFWBueJW1ATmCG9tJOtETnQTUdY", {
|
posthog.init("phc_hTnel2Q8GKo0TgIBnFWBueJW1ATmCG9tJOtETnQTUdY", {
|
||||||
@@ -17,7 +17,23 @@
|
|||||||
invalidate("supabase:auth");
|
invalidate("supabase:auth");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return () => data.subscription.unsubscribe();
|
const spacesChannel = supabase
|
||||||
|
.channel("study_space_updates")
|
||||||
|
.on("broadcast", { event: "study_space_updated" }, () => {
|
||||||
|
invalidate("db:study_spaces");
|
||||||
|
})
|
||||||
|
.subscribe();
|
||||||
|
return () => {
|
||||||
|
data.subscription.unsubscribe();
|
||||||
|
spacesChannel.unsubscribe();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
$effect(() => {
|
||||||
|
if (route.id === "/filter") {
|
||||||
|
document.body.classList.add("coloured");
|
||||||
|
} else {
|
||||||
|
document.body.classList.remove("coloured");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -33,7 +49,7 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow-x: hidden;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(html) {
|
:global(html) {
|
||||||
@@ -41,6 +57,10 @@
|
|||||||
color: #eaffeb;
|
color: #eaffeb;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(body.coloured) {
|
||||||
|
background: linear-gradient(-77deg, #2e4653, #223a37);
|
||||||
|
}
|
||||||
|
|
||||||
:global(*) {
|
:global(*) {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-family: Inter;
|
font-family: Inter;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { PUBLIC_SUPABASE_ANON_KEY, PUBLIC_SUPABASE_URL } from "$env/static/publi
|
|||||||
import type { Database } from "$lib/database";
|
import type { Database } from "$lib/database";
|
||||||
import type { LayoutLoad } from "./$types";
|
import type { LayoutLoad } from "./$types";
|
||||||
|
|
||||||
export const load: LayoutLoad = async ({ data, depends, fetch }) => {
|
export const load: LayoutLoad = async ({ data, url, route, depends, fetch }) => {
|
||||||
/**
|
/**
|
||||||
* Declare a dependency so the layout can be invalidated, for example, on
|
* Declare a dependency so the layout can be invalidated, for example, on
|
||||||
* session refresh.
|
* session refresh.
|
||||||
@@ -40,5 +40,12 @@ export const load: LayoutLoad = async ({ data, depends, fetch }) => {
|
|||||||
data: { user }
|
data: { user }
|
||||||
} = await supabase.auth.getUser();
|
} = await supabase.auth.getUser();
|
||||||
|
|
||||||
return { session, supabase, user, adminMode: data.adminMode };
|
return {
|
||||||
|
session,
|
||||||
|
supabase,
|
||||||
|
user,
|
||||||
|
adminMode: data.adminMode,
|
||||||
|
route,
|
||||||
|
searchParams: url.searchParams.toString()
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,7 +8,25 @@ export const load: PageServerLoad = async ({ depends, locals: { supabase } }) =>
|
|||||||
.select("*, study_space_images(*), study_space_hours(*)");
|
.select("*, study_space_images(*), study_space_hours(*)");
|
||||||
if (err) error(500, "Failed to load study spaces");
|
if (err) error(500, "Failed to load study spaces");
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: { session }
|
||||||
|
} = await supabase.auth.getSession();
|
||||||
|
|
||||||
|
// Fetch this user’s favourites
|
||||||
|
let favouriteIds: string[] = [];
|
||||||
|
if (session?.user?.id) {
|
||||||
|
const { data: favs, error: favErr } = await supabase
|
||||||
|
.from("favourite_study_spaces")
|
||||||
|
.select("study_space_id")
|
||||||
|
.eq("user_id", session.user.id);
|
||||||
|
if (!favErr && favs) {
|
||||||
|
favouriteIds = favs.map((f) => f.study_space_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
studySpaces
|
studySpaces,
|
||||||
|
session,
|
||||||
|
favouriteIds
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,48 +2,56 @@
|
|||||||
import SpaceCard from "$lib/components/SpaceCard.svelte";
|
import SpaceCard from "$lib/components/SpaceCard.svelte";
|
||||||
import defaultImg from "$lib/assets/study_space.png";
|
import defaultImg from "$lib/assets/study_space.png";
|
||||||
import crossUrl from "$lib/assets/cross.svg";
|
import crossUrl from "$lib/assets/cross.svg";
|
||||||
|
import searchUrl from "$lib/assets/search.svg";
|
||||||
import Navbar from "$lib/components/Navbar.svelte";
|
import Navbar from "$lib/components/Navbar.svelte";
|
||||||
import { allTags, volumeTags, wifiTags, powerOutletTags } from "$lib";
|
import { collectTimings, timeToMins, haversineDistance } from "$lib";
|
||||||
import Button from "$lib/components/Button.svelte";
|
import Button from "$lib/components/Button.svelte";
|
||||||
|
import { urldecodeSortFilter } from "$lib/filter.js";
|
||||||
|
import { invalidateAll } from "$app/navigation";
|
||||||
|
import type { Table } from "$lib";
|
||||||
|
|
||||||
const { data } = $props();
|
const { data } = $props();
|
||||||
const { studySpaces, supabase, session, adminMode } = $derived(data);
|
const {
|
||||||
|
studySpaces,
|
||||||
|
supabase,
|
||||||
|
session,
|
||||||
|
adminMode,
|
||||||
|
searchParams,
|
||||||
|
favouriteIds: initialFavourites = []
|
||||||
|
} = $derived(data);
|
||||||
|
|
||||||
let selectedTags = $state<string[]>([]);
|
let favouriteIds = $derived<string[]>(initialFavourites);
|
||||||
let tagFilter = $state("");
|
let showFavourites = $state(false);
|
||||||
let openingFilter = $state("");
|
|
||||||
let closingFilter = $state("");
|
|
||||||
let tagFilterElem = $state<HTMLInputElement>();
|
|
||||||
|
|
||||||
function categorySelected(category: string[]) {
|
const sortFilter = $derived(urldecodeSortFilter(searchParams));
|
||||||
return category.some((tag) => selectedTags.includes(tag));
|
const selectedTags = $derived(sortFilter.tags ?? []);
|
||||||
|
const openingFilter = $derived(sortFilter.openAt?.from);
|
||||||
|
const closingFilter = $derived(sortFilter.openAt?.to);
|
||||||
|
const sortNear = $derived(sortFilter.nearby);
|
||||||
|
|
||||||
|
// Toggle a space in/out of favourites
|
||||||
|
async function handleToggleFavourite(id: string) {
|
||||||
|
if (!session?.user) return;
|
||||||
|
const already = favouriteIds.includes(id);
|
||||||
|
if (already) {
|
||||||
|
await supabase
|
||||||
|
.from("favourite_study_spaces")
|
||||||
|
.delete()
|
||||||
|
.match({ user_id: session.user.id, study_space_id: id });
|
||||||
|
favouriteIds = favouriteIds.filter((x) => x !== id);
|
||||||
|
} else {
|
||||||
|
await supabase
|
||||||
|
.from("favourite_study_spaces")
|
||||||
|
.insert([{ user_id: session.user.id, study_space_id: id }]);
|
||||||
|
favouriteIds = [...favouriteIds, id];
|
||||||
}
|
}
|
||||||
|
|
||||||
let filteredTags = $derived(
|
|
||||||
allTags
|
|
||||||
.filter((tag) => tag.toLowerCase().includes(tagFilter.toLowerCase()))
|
|
||||||
.filter((tag) => !selectedTags.includes(tag))
|
|
||||||
.filter((tag) => {
|
|
||||||
if (selectedTags.includes(tag)) return false;
|
|
||||||
|
|
||||||
if (categorySelected(volumeTags) && volumeTags.includes(tag)) return false;
|
|
||||||
if (categorySelected(wifiTags) && wifiTags.includes(tag)) return false;
|
|
||||||
if (categorySelected(powerOutletTags) && powerOutletTags.includes(tag))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// Convert "HH:MM" or "HH:MM:SS" to minutes since midnight
|
|
||||||
function toMinutes(timeStr: string): number {
|
|
||||||
const [h, m] = timeStr.slice(0, 5).split(":").map(Number);
|
|
||||||
return h * 60 + m;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Combine tag and time filtering
|
// Combine tag and time filtering
|
||||||
let filteredStudySpaces = $derived(
|
let filteredStudySpaces = $derived(
|
||||||
studySpaces
|
studySpaces
|
||||||
|
// only include favourites when showFavourites===true
|
||||||
|
.filter((space) => !showFavourites || favouriteIds?.includes(space.id))
|
||||||
// tag filtering
|
// tag filtering
|
||||||
.filter((space) => {
|
.filter((space) => {
|
||||||
if (selectedTags.length === 0) return true;
|
if (selectedTags.length === 0) return true;
|
||||||
@@ -62,13 +70,13 @@
|
|||||||
(h) => h.day_of_week === new Date().getDay()
|
(h) => h.day_of_week === new Date().getDay()
|
||||||
);
|
);
|
||||||
if (!entry) return false;
|
if (!entry) return false;
|
||||||
if (entry.is_24_7) return true;
|
if (entry.open_today_status) return true;
|
||||||
const openMin = toMinutes(entry.opens_at);
|
const openMin = timeToMins(entry.opens_at);
|
||||||
let closeMin = toMinutes(entry.closes_at);
|
let closeMin = timeToMins(entry.closes_at);
|
||||||
// Treat midnight as end of day and handle overnight spans
|
// Treat midnight as end of day and handle overnight spans
|
||||||
if (closeMin === 0) closeMin = 24 * 60;
|
if (closeMin === 0) closeMin = 24 * 60;
|
||||||
if (closeMin <= openMin) closeMin += 24 * 60;
|
if (closeMin <= openMin) closeMin += 24 * 60;
|
||||||
const filterMin = toMinutes(openingFilter);
|
const filterMin = timeToMins(openingFilter);
|
||||||
// Include spaces open at the filter time
|
// Include spaces open at the filter time
|
||||||
return filterMin >= openMin && filterMin < closeMin;
|
return filterMin >= openMin && filterMin < closeMin;
|
||||||
})
|
})
|
||||||
@@ -79,142 +87,187 @@
|
|||||||
(h) => h.day_of_week === new Date().getDay()
|
(h) => h.day_of_week === new Date().getDay()
|
||||||
);
|
);
|
||||||
if (!entry) return false;
|
if (!entry) return false;
|
||||||
if (entry.is_24_7) return true;
|
if (entry.open_today_status) return true;
|
||||||
const openMin = toMinutes(entry.opens_at);
|
const openMin = timeToMins(entry.opens_at);
|
||||||
let closeMin = toMinutes(entry.closes_at);
|
let closeMin = timeToMins(entry.closes_at);
|
||||||
if (closeMin === 0) closeMin = 24 * 60;
|
if (closeMin === 0) closeMin = 24 * 60;
|
||||||
if (closeMin <= openMin) closeMin += 24 * 60;
|
if (closeMin <= openMin) closeMin += 24 * 60;
|
||||||
const filterMin =
|
const filterMin =
|
||||||
toMinutes(closingFilter) === 0 ? 24 * 60 : toMinutes(closingFilter);
|
timeToMins(closingFilter) === 0 ? 24 * 60 : timeToMins(closingFilter);
|
||||||
// Include spaces still open at the filter time
|
// Include spaces still open at the filter time
|
||||||
return filterMin > openMin && filterMin <= closeMin;
|
return filterMin > openMin && filterMin <= closeMin;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
let dropdownVisible = $state(false);
|
const sortedByOpenNow = $derived(
|
||||||
|
filteredStudySpaces.toSorted((a, b) => {
|
||||||
|
const now = new Date();
|
||||||
|
const time = now.toTimeString().slice(0, 5);
|
||||||
|
const today = now.getDay();
|
||||||
|
let openUntil = [0, 0] as number[];
|
||||||
|
for (const [index, day] of [a, b].entries()) {
|
||||||
|
const timingsPerDay = collectTimings(day.study_space_hours);
|
||||||
|
for (const timing of timingsPerDay[today]) {
|
||||||
|
if (timing.open_today_status === true) {
|
||||||
|
openUntil[index] = 24 * 60;
|
||||||
|
break;
|
||||||
|
} else if (timing.open_today_status === false) {
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
const opensFor = timeUntilClosing(timing.opens_at, timing.closes_at, time);
|
||||||
|
if (opensFor) {
|
||||||
|
openUntil[index] = opensFor;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return openUntil[1] - openUntil[0];
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
function deleteTag(tagName: string) {
|
const sortedStudySpaces = $derived(
|
||||||
return () => {
|
sortNear
|
||||||
selectedTags = selectedTags.filter((tag) => tag !== tagName);
|
? filteredStudySpaces.toSorted((a, b) => {
|
||||||
|
if (!sortNear) return 0;
|
||||||
|
type DBLatLng = { lat: number; lng: number } | undefined;
|
||||||
|
const aLoc = a.building_location as unknown as google.maps.places.PlaceResult;
|
||||||
|
const bLoc = b.building_location as unknown as google.maps.places.PlaceResult;
|
||||||
|
const aLatLng = aLoc.geometry?.location as DBLatLng;
|
||||||
|
const bLatLng = bLoc.geometry?.location as DBLatLng;
|
||||||
|
const aDistance = haversineDistance(
|
||||||
|
sortNear.lat,
|
||||||
|
sortNear.lng,
|
||||||
|
aLatLng?.lat || sortNear.lat,
|
||||||
|
aLatLng?.lng || sortNear.lng
|
||||||
|
);
|
||||||
|
const bDistance = haversineDistance(
|
||||||
|
sortNear.lat,
|
||||||
|
sortNear.lng,
|
||||||
|
bLatLng?.lat || sortNear.lat,
|
||||||
|
bLatLng?.lng || sortNear.lng
|
||||||
|
);
|
||||||
|
return aDistance - bDistance;
|
||||||
|
})
|
||||||
|
: sortedByOpenNow
|
||||||
|
);
|
||||||
|
|
||||||
|
// Open now
|
||||||
|
function isOpenNow(all_study_space_hours: Table<"study_space_hours">[]) {
|
||||||
|
const now = new Date();
|
||||||
|
const time = now.toTimeString().slice(0, 5);
|
||||||
|
const day = now.getDay();
|
||||||
|
|
||||||
|
const timingsPerDay = collectTimings(all_study_space_hours);
|
||||||
|
for (const timing of timingsPerDay[day]) {
|
||||||
|
if (timing.open_today_status === true) {
|
||||||
|
return { isOpen: true, message: `Open all day` };
|
||||||
|
} else if (timing.open_today_status === false) {
|
||||||
|
return { isOpen: false, message: `Closed today` };
|
||||||
|
} else {
|
||||||
|
const opensFor = timeUntilClosing(timing.opens_at, timing.closes_at, time);
|
||||||
|
if (opensFor) {
|
||||||
|
return {
|
||||||
|
isOpen: true,
|
||||||
|
message: `Open now for: ${minsToReadableHours(opensFor)}`
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function addTag(tagName: string) {
|
return { isOpen: false, message: "Closed right now" };
|
||||||
return () => {
|
|
||||||
if (!selectedTags.includes(tagName)) {
|
|
||||||
selectedTags.push(tagName);
|
|
||||||
}
|
}
|
||||||
tagFilter = "";
|
|
||||||
};
|
function timeUntilClosing(openingTime: string, closingTime: string, currentTime: string) {
|
||||||
|
const currTimeInMins = timeToMins(currentTime);
|
||||||
|
const OpeningTimeInMins = timeToMins(openingTime);
|
||||||
|
const closingTimeInMins = timeToMins(closingTime);
|
||||||
|
if (currTimeInMins >= OpeningTimeInMins && currTimeInMins < closingTimeInMins) {
|
||||||
|
return closingTimeInMins - currTimeInMins;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function minsToReadableHours(mins: number) {
|
||||||
|
return `${Math.floor(mins / 60)} hrs, ${mins % 60} mins`;
|
||||||
|
}
|
||||||
|
|
||||||
|
$inspect(sortedStudySpaces);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Navbar>
|
<Navbar>
|
||||||
<div class="navActions">
|
|
||||||
<div class="tagDisplay">
|
|
||||||
{#each selectedTags as tagName (tagName)}
|
|
||||||
<button class="tag" onclick={deleteTag(tagName)} type="button">
|
|
||||||
{tagName}
|
|
||||||
<img src={crossUrl} alt="delete" /></button
|
|
||||||
>
|
|
||||||
{/each}
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="tagInput"
|
|
||||||
class="tagInput"
|
|
||||||
bind:value={tagFilter}
|
|
||||||
bind:this={tagFilterElem}
|
|
||||||
onfocus={() => {
|
|
||||||
dropdownVisible = true;
|
|
||||||
}}
|
|
||||||
onblur={() => {
|
|
||||||
dropdownVisible = false;
|
|
||||||
}}
|
|
||||||
onkeypress={(event) => {
|
|
||||||
if (event.key === "Enter") {
|
|
||||||
event.preventDefault();
|
|
||||||
const tag = filteredTags[0];
|
|
||||||
if (tag) addTag(tag)();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
placeholder="Search by tags..."
|
|
||||||
/>
|
|
||||||
{#if dropdownVisible}
|
|
||||||
<div class="tagDropdown">
|
|
||||||
{#each filteredTags as avaliableTag (avaliableTag)}
|
|
||||||
<button
|
|
||||||
class="avaliableTag"
|
|
||||||
onclick={addTag(avaliableTag)}
|
|
||||||
onmousedown={(e) => {
|
|
||||||
// Keep input focused
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
}}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
{avaliableTag}
|
|
||||||
</button>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{#if session}
|
{#if session}
|
||||||
<a href="/space/new/edit">
|
<a href="/space/new/edit">
|
||||||
<img src={crossUrl} alt="new" class="new-space" />
|
<img src={crossUrl} alt="new" class="new-space" />
|
||||||
</a>
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if adminMode}
|
||||||
|
<span class="checkReports">
|
||||||
|
<Button href="/space/reports" type="link" style="red">Check Reports</Button>
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
{#if session}
|
||||||
|
<button class="fav-button" onclick={() => (showFavourites = !showFavourites)} type="button">
|
||||||
|
{showFavourites ? "All spaces" : "My favourites"}
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="filterWrapper">
|
||||||
|
<Button type="link" href="/filter?{searchParams}">
|
||||||
|
<span class="search">
|
||||||
|
<img src={searchUrl} alt="search" />
|
||||||
|
Search
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Navbar>
|
</Navbar>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<div class="time-filter-container">
|
{#each sortedStudySpaces as studySpace (studySpace.id)}
|
||||||
<label>
|
<SpaceCard
|
||||||
Open from:
|
alt="Photo of {studySpace.description}"
|
||||||
<input type="time" bind:value={openingFilter} />
|
href="/space/{studySpace.id}"
|
||||||
</label>
|
imgSrc={studySpace.study_space_images.length > 0
|
||||||
<label>
|
|
||||||
Open until:
|
|
||||||
<input type="time" bind:value={closingFilter} />
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
{#if adminMode}
|
|
||||||
<div class="checkReports">
|
|
||||||
<Button href="/space/reports" type="link" style="red">Check Reports</Button>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{#each filteredStudySpaces as studySpace (studySpace.id)}
|
|
||||||
{@const imgUrl =
|
|
||||||
studySpace.study_space_images.length > 0
|
|
||||||
? supabase.storage
|
? supabase.storage
|
||||||
.from("files_bucket")
|
.from("files_bucket")
|
||||||
.getPublicUrl(studySpace.study_space_images[0].image_path).data.publicUrl
|
.getPublicUrl(studySpace.study_space_images[0].image_path).data.publicUrl
|
||||||
: defaultImg}
|
: defaultImg}
|
||||||
<SpaceCard
|
|
||||||
alt="Photo of {studySpace.description}"
|
|
||||||
href="/space/{studySpace.id}"
|
|
||||||
imgSrc={imgUrl}
|
|
||||||
space={studySpace}
|
space={studySpace}
|
||||||
hours={studySpace.study_space_hours}
|
isFavourite={favouriteIds.includes(studySpace.id)}
|
||||||
|
onToggleFavourite={session ? () => handleToggleFavourite(studySpace.id) : undefined}
|
||||||
|
isAvailable={studySpace.study_space_hours.length === 0
|
||||||
|
? undefined
|
||||||
|
: isOpenNow(studySpace.study_space_hours).isOpen}
|
||||||
|
footer={studySpace.study_space_hours.length === 0
|
||||||
|
? undefined
|
||||||
|
: isOpenNow(studySpace.study_space_hours).message}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
{#if session}
|
{#if session}
|
||||||
<Button onclick={() => supabase.auth.signOut()}>Signout</Button>
|
<Button
|
||||||
|
onclick={async () => {
|
||||||
|
await supabase.auth.signOut();
|
||||||
|
invalidateAll();
|
||||||
|
}}>Signout</Button
|
||||||
|
>
|
||||||
{:else}
|
{:else}
|
||||||
<Button href="/auth" type="link">Login / Signup</Button>
|
<Button href="/auth" type="link">Login / Signup</Button>
|
||||||
{/if}
|
{/if}
|
||||||
</footer>
|
</footer>
|
||||||
|
{#if adminMode}
|
||||||
|
<div class="adminMode">You are in admin mode</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
main {
|
main {
|
||||||
display: grid;
|
display: grid;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
gap: 1rem;
|
gap: 0.5rem;
|
||||||
padding: 1rem;
|
padding: 0.5rem;
|
||||||
max-width: 600px;
|
max-width: 32rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
@@ -232,122 +285,43 @@
|
|||||||
transform: rotate(45deg);
|
transform: rotate(45deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.navActions {
|
|
||||||
display: flex;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
align-items: center;
|
|
||||||
padding-left: 2rem;
|
|
||||||
gap: 1rem;
|
|
||||||
z-index: 200;
|
|
||||||
}
|
|
||||||
|
|
||||||
.time-filter-container {
|
|
||||||
grid-column: 1 / -1;
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
|
||||||
justify-content: center;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
.time-filter-container label {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
font-size: 1rem;
|
|
||||||
color: #eaffeb;
|
|
||||||
}
|
|
||||||
.time-filter-container input[type="time"] {
|
|
||||||
background: none;
|
|
||||||
border: 2px solid #eaffeb;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
padding: 0.25rem 0.5rem;
|
|
||||||
color: #eaffeb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tagDisplay {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.4rem;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: left;
|
|
||||||
justify-content: left;
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
height: auto;
|
|
||||||
padding: 0.5rem;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
border: 2px solid #eaffeb;
|
|
||||||
background: none;
|
|
||||||
color: #eaffeb;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
.tagInput {
|
|
||||||
flex: 1 1 100%;
|
|
||||||
min-width: 10rem;
|
|
||||||
background: none;
|
|
||||||
color: #eaffeb;
|
|
||||||
font-size: 1rem;
|
|
||||||
border: none;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
::placeholder {
|
|
||||||
color: #859a90;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
.tag {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
background-color: #2e4653;
|
|
||||||
color: #eaffeb;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
cursor: pointer;
|
|
||||||
border-width: 0rem;
|
|
||||||
}
|
|
||||||
.tag img {
|
|
||||||
width: 1rem;
|
|
||||||
height: 1rem;
|
|
||||||
margin-left: 0.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tagDropdown {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
gap: 0.4rem;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
position: absolute;
|
|
||||||
background-color: #2e4653;
|
|
||||||
box-shadow: 1px 1px 0.5rem rgba(0, 0, 0, 0.5);
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
overflow-y: auto;
|
|
||||||
max-height: 10rem;
|
|
||||||
top: 100%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
z-index: 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avaliableTag {
|
|
||||||
width: 100%;
|
|
||||||
text-align: left;
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: #eaffeb;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
margin: 0%;
|
|
||||||
padding: 0 0.8rem 0.4rem;
|
|
||||||
}
|
|
||||||
.avaliableTag:first-child {
|
|
||||||
padding-top: 0.6rem;
|
|
||||||
background-color: hsl(201, 26%, 60%);
|
|
||||||
}
|
|
||||||
.avaliableTag:last-child {
|
|
||||||
padding-bottom: 0.6rem;
|
|
||||||
}
|
|
||||||
.checkReports {
|
.checkReports {
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
font-size: 1.2rem;
|
font-size: 1rem;
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fav-button {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #eaffeb;
|
||||||
|
font-size: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.fav-button:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterWrapper {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.3rem;
|
||||||
|
color: #eaffeb;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search img {
|
||||||
|
width: 1.2rem;
|
||||||
|
height: 1.2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 20rem) {
|
@media (max-width: 20rem) {
|
||||||
@@ -355,4 +329,24 @@
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.adminMode {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
position: sticky;
|
||||||
|
left: 0;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
background-color: #182125;
|
||||||
|
bottom: 0;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.1);
|
||||||
|
color: #eaffeb;
|
||||||
|
border: 2px solid #eaffeb;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
362
src/routes/filter/+page.svelte
Normal file
@@ -0,0 +1,362 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { allTags, volumeTags, wifiTags, powerOutletTags, gmapsLoader } from "$lib";
|
||||||
|
import crossUrl from "$lib/assets/cross.svg";
|
||||||
|
import Button from "$lib/components/Button.svelte";
|
||||||
|
import Navbar from "$lib/components/Navbar.svelte";
|
||||||
|
import { urldecodeSortFilter, urlencodeSortFilter, type SortFiler } from "$lib/filter.js";
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
|
||||||
|
const { data } = $props();
|
||||||
|
const { searchParams } = $derived(data);
|
||||||
|
const sortFilter = $derived(urldecodeSortFilter(searchParams));
|
||||||
|
|
||||||
|
// svelte-ignore state_referenced_locally
|
||||||
|
const openAt = $state(sortFilter.openAt ?? ({} as Partial<SortFiler["openAt"]>));
|
||||||
|
// svelte-ignore state_referenced_locally
|
||||||
|
let selectedTags = $state(sortFilter.tags ?? ([] as SortFiler["tags"]));
|
||||||
|
// svelte-ignore state_referenced_locally
|
||||||
|
let sortNear = $state(sortFilter.nearby ?? undefined);
|
||||||
|
|
||||||
|
let tagFilter = $state("");
|
||||||
|
|
||||||
|
const newSearchParams = $derived(
|
||||||
|
urlencodeSortFilter({
|
||||||
|
openAt: openAt?.from ? (openAt as { from: string; to?: string }) : undefined,
|
||||||
|
tags: selectedTags.length > 0 ? selectedTags : undefined,
|
||||||
|
nearby: sortNear
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
let filteredTags = $derived(
|
||||||
|
allTags
|
||||||
|
.filter((tag) => tag.toLowerCase().includes(tagFilter.toLowerCase()))
|
||||||
|
.filter((tag) => !selectedTags.includes(tag))
|
||||||
|
.filter((tag) => {
|
||||||
|
if (selectedTags.includes(tag)) return false;
|
||||||
|
|
||||||
|
if (categorySelected(volumeTags) && volumeTags.includes(tag)) return false;
|
||||||
|
if (categorySelected(wifiTags) && wifiTags.includes(tag)) return false;
|
||||||
|
if (categorySelected(powerOutletTags) && powerOutletTags.includes(tag))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
let dropdownVisible = $state(false);
|
||||||
|
|
||||||
|
function deleteTag(tagName: string) {
|
||||||
|
return () => {
|
||||||
|
selectedTags = selectedTags.filter((tag) => tag !== tagName);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function addTag(tagName: string) {
|
||||||
|
return () => {
|
||||||
|
if (!selectedTags.includes(tagName)) {
|
||||||
|
selectedTags.push(tagName);
|
||||||
|
}
|
||||||
|
tagFilter = "";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let sortMapElem = $state<HTMLDivElement>();
|
||||||
|
let marker = $state<google.maps.marker.AdvancedMarkerElement>();
|
||||||
|
onMount(async () => {
|
||||||
|
if (!sortMapElem) return console.error("sortMapElem is not defined");
|
||||||
|
const loader = await gmapsLoader();
|
||||||
|
const { Map } = await loader.importLibrary("maps");
|
||||||
|
const { AdvancedMarkerElement } = await loader.importLibrary("marker");
|
||||||
|
const map = new Map(sortMapElem, {
|
||||||
|
center: { lat: 51.5087393, lng: -0.1667442 },
|
||||||
|
zoom: 10,
|
||||||
|
mapId: "9f4993cd3fb1504d495821a5"
|
||||||
|
});
|
||||||
|
marker = new AdvancedMarkerElement({
|
||||||
|
map,
|
||||||
|
title: "Find near here"
|
||||||
|
});
|
||||||
|
map.addListener("click", (e: google.maps.MapMouseEvent) => {
|
||||||
|
console.log("Clicked map at", e.latLng);
|
||||||
|
sortNear = e.latLng
|
||||||
|
? {
|
||||||
|
lat: e.latLng.lat(),
|
||||||
|
lng: e.latLng.lng()
|
||||||
|
}
|
||||||
|
: sortNear;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
$effect(() => {
|
||||||
|
if (marker) {
|
||||||
|
marker.position = sortNear;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function categorySelected(category: string[]) {
|
||||||
|
return category.some((tag) => selectedTags.includes(tag));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Navbar>
|
||||||
|
<a href="/?{searchParams}">
|
||||||
|
<img src={crossUrl} alt="close" />
|
||||||
|
</a>
|
||||||
|
</Navbar>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<h1>Search options</h1>
|
||||||
|
<div class="time-filter-container">
|
||||||
|
<label>
|
||||||
|
Open from
|
||||||
|
<input type="time" bind:value={openAt.from} />
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
until
|
||||||
|
<input type="time" bind:value={openAt.to} />
|
||||||
|
</label>
|
||||||
|
<span class="setToNow">
|
||||||
|
<Button
|
||||||
|
onclick={() => {
|
||||||
|
const now = new Date();
|
||||||
|
openAt.from = `${now.getHours().toString().padStart(2, "0")}:${now.getMinutes().toString().padStart(2, "0")}`;
|
||||||
|
openAt.to = undefined;
|
||||||
|
console.log(openAt);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Set to now
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="tag-filter-container">
|
||||||
|
<div class="tagDisplay">
|
||||||
|
{#each selectedTags as tagName, idx (tagName + idx)}
|
||||||
|
<button class="tag" onclick={deleteTag(tagName)} type="button">
|
||||||
|
{tagName}
|
||||||
|
<img src={crossUrl} alt="delete" /></button
|
||||||
|
>
|
||||||
|
{/each}
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="tagInput"
|
||||||
|
class="tagInput"
|
||||||
|
bind:value={tagFilter}
|
||||||
|
onfocus={() => {
|
||||||
|
dropdownVisible = true;
|
||||||
|
}}
|
||||||
|
onblur={() => {
|
||||||
|
dropdownVisible = false;
|
||||||
|
}}
|
||||||
|
onkeypress={(event) => {
|
||||||
|
if (event.key === "Enter") {
|
||||||
|
event.preventDefault();
|
||||||
|
const tag = filteredTags[0];
|
||||||
|
if (tag) addTag(tag)();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder="Search by tags..."
|
||||||
|
/>
|
||||||
|
{#if dropdownVisible}
|
||||||
|
<div class="tagDropdown">
|
||||||
|
{#each filteredTags as avaliableTag, idx (avaliableTag + idx)}
|
||||||
|
<button
|
||||||
|
class="avaliableTag"
|
||||||
|
onclick={addTag(avaliableTag)}
|
||||||
|
onmousedown={(e) => {
|
||||||
|
// Keep input focused
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{avaliableTag}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="location-filter-container">
|
||||||
|
<h3 class="location-filter-title">Click to search nearby</h3>
|
||||||
|
<Button
|
||||||
|
onclick={() => {
|
||||||
|
navigator.geolocation.getCurrentPosition((position) => {
|
||||||
|
if (marker)
|
||||||
|
sortNear = marker.position = {
|
||||||
|
lat: position.coords.latitude,
|
||||||
|
lng: position.coords.longitude
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Use current location
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div class="sortMap" bind:this={sortMapElem}></div>
|
||||||
|
</main>
|
||||||
|
<div class="controls">
|
||||||
|
<div class="controls-inner">
|
||||||
|
<Button type="link" href="/?{newSearchParams}">Back to study spaces</Button>
|
||||||
|
<Button
|
||||||
|
style="red"
|
||||||
|
onclick={() => {
|
||||||
|
openAt.from = undefined;
|
||||||
|
openAt.to = undefined;
|
||||||
|
selectedTags = [];
|
||||||
|
sortNear = undefined;
|
||||||
|
tagFilter = "";
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
main {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
max-width: 32rem;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
.controls {
|
||||||
|
position: sticky;
|
||||||
|
background: inherit;
|
||||||
|
background-attachment: local;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.controls-inner {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
max-width: 32rem;
|
||||||
|
gap: 1rem;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
.tag-filter-container {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-filter-container {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
.location-filter-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
.time-filter-container label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
color: #eaffeb;
|
||||||
|
}
|
||||||
|
.time-filter-container input[type="time"] {
|
||||||
|
background: none;
|
||||||
|
border: 2px solid #000000;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
color: #000000;
|
||||||
|
filter: brightness(0) saturate(100%) invert(98%) sepia(8%) saturate(555%) hue-rotate(54deg)
|
||||||
|
brightness(100%) contrast(102%);
|
||||||
|
}
|
||||||
|
.setToNow {
|
||||||
|
display: flex;
|
||||||
|
justify-content: end;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tagDisplay {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.4rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: left;
|
||||||
|
justify-content: left;
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
border: 2px solid #eaffeb;
|
||||||
|
background: none;
|
||||||
|
color: #eaffeb;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
.tagInput {
|
||||||
|
flex: 1 1 100%;
|
||||||
|
min-width: 10rem;
|
||||||
|
background: none;
|
||||||
|
color: #eaffeb;
|
||||||
|
font-size: 1rem;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
::placeholder {
|
||||||
|
color: #859a90;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.tag {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
background-color: #2e4653;
|
||||||
|
color: #eaffeb;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
cursor: pointer;
|
||||||
|
border-width: 0rem;
|
||||||
|
}
|
||||||
|
.tag img {
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
margin-left: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tagDropdown {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.4rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
position: absolute;
|
||||||
|
background-color: #2e4653;
|
||||||
|
box-shadow: 1px 1px 0.5rem rgba(0, 0, 0, 0.5);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: 10rem;
|
||||||
|
top: 100%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avaliableTag {
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #eaffeb;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin: 0%;
|
||||||
|
padding: 0 0.8rem 0.4rem;
|
||||||
|
}
|
||||||
|
.avaliableTag:first-child {
|
||||||
|
padding-top: 0.6rem;
|
||||||
|
background-color: hsl(201, 26%, 60%);
|
||||||
|
}
|
||||||
|
.avaliableTag:last-child {
|
||||||
|
padding-bottom: 0.6rem;
|
||||||
|
}
|
||||||
|
.location-filter-title {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.sortMap {
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -7,8 +7,9 @@
|
|||||||
import Report from "$lib/components/Report.svelte";
|
import Report from "$lib/components/Report.svelte";
|
||||||
import Feedback from "$lib/components/Feedback.svelte";
|
import Feedback from "$lib/components/Feedback.svelte";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { gmapsLoader, daysOfWeek, formatTime } from "$lib";
|
import { gmapsLoader, daysOfWeek, formatTime, collectTimings } from "$lib";
|
||||||
import Button from "$lib/components/Button.svelte";
|
import Button from "$lib/components/Button.svelte";
|
||||||
|
import Favourite from "$lib/components/Favourite.svelte";
|
||||||
|
|
||||||
const { data } = $props();
|
const { data } = $props();
|
||||||
const { space, supabase, adminMode } = $derived(data);
|
const { space, supabase, adminMode } = $derived(data);
|
||||||
@@ -51,12 +52,47 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const hoursByDay = $derived(new Map(space.study_space_hours.map((h) => [h.day_of_week, h])));
|
let timingsPerDay = collectTimings(space.study_space_hours);
|
||||||
|
|
||||||
const openingEntries = daysOfWeek.map((day, idx) => ({
|
let isFavourite = $state(false);
|
||||||
day,
|
onMount(async () => {
|
||||||
entry: hoursByDay.get(idx)
|
const {
|
||||||
}));
|
data: { session }
|
||||||
|
} = await supabase.auth.getSession();
|
||||||
|
if (!session?.user) return;
|
||||||
|
const { data: fav } = await supabase
|
||||||
|
.from("favourite_study_spaces")
|
||||||
|
.select("study_space_id")
|
||||||
|
.match({ user_id: session.user.id, study_space_id: space.id })
|
||||||
|
.single();
|
||||||
|
isFavourite = !!fav;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Toggle a space in/out of favourites
|
||||||
|
async function handleToggleFavourite() {
|
||||||
|
const {
|
||||||
|
data: { session }
|
||||||
|
} = await supabase.auth.getSession();
|
||||||
|
if (!session?.user) return;
|
||||||
|
if (isFavourite) {
|
||||||
|
await supabase
|
||||||
|
.from("favourite_study_spaces")
|
||||||
|
.delete()
|
||||||
|
.match({ user_id: session.user.id, study_space_id: space.id });
|
||||||
|
isFavourite = false;
|
||||||
|
} else {
|
||||||
|
await supabase
|
||||||
|
.from("favourite_study_spaces")
|
||||||
|
.insert([{ user_id: session.user.id, study_space_id: space.id }]);
|
||||||
|
isFavourite = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteSpace() {
|
||||||
|
if (!confirm("Are you sure you want to delete this study space?")) return;
|
||||||
|
await supabase.from("study_spaces").delete().eq("id", space.id);
|
||||||
|
window.location.href = "/";
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Navbar>
|
<Navbar>
|
||||||
@@ -78,9 +114,22 @@
|
|||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
<main>
|
<main>
|
||||||
|
<div class="imgContainer">
|
||||||
|
{#await supabase.auth.getSession() then resp}
|
||||||
|
{#if resp.data.session}
|
||||||
|
<div class="title-fav">
|
||||||
|
<Favourite
|
||||||
|
{isFavourite}
|
||||||
|
onToggleFavourite={handleToggleFavourite}
|
||||||
|
imgSize={27}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/await}
|
||||||
<Carousel urls={imgUrls} />
|
<Carousel urls={imgUrls} />
|
||||||
|
</div>
|
||||||
<div class="nameContainer">
|
<div class="nameContainer">
|
||||||
{space.location}
|
<div class="locationContainer">{space.location}</div>
|
||||||
</div>
|
</div>
|
||||||
{#if space.description != null && space.description.length > 0}
|
{#if space.description != null && space.description.length > 0}
|
||||||
<p class="descContainer">
|
<p class="descContainer">
|
||||||
@@ -91,7 +140,7 @@
|
|||||||
<div class="compulsoryContainer"><CompulsoryTags {space} /></div>
|
<div class="compulsoryContainer"><CompulsoryTags {space} /></div>
|
||||||
{#if space.tags.length > 0}
|
{#if space.tags.length > 0}
|
||||||
<div class="tagContainer">
|
<div class="tagContainer">
|
||||||
{#each space.tags as tag (tag)}
|
{#each space.tags as tag, idx (tag + idx)}
|
||||||
<span class="tag">
|
<span class="tag">
|
||||||
{tag}
|
{tag}
|
||||||
</span>
|
</span>
|
||||||
@@ -100,27 +149,38 @@
|
|||||||
{/if}
|
{/if}
|
||||||
<hr />
|
<hr />
|
||||||
<div class="subtitle">Opening Times:</div>
|
<div class="subtitle">Opening Times:</div>
|
||||||
{#each openingEntries as { day, entry } (entry)}
|
{#each Array(7).keys() as idx (idx)}
|
||||||
|
{@const entries = timingsPerDay[idx]}
|
||||||
<div class="opening-entry">
|
<div class="opening-entry">
|
||||||
<span class="day">{day}:</span>
|
<span class="day">{daysOfWeek[idx]}</span>
|
||||||
<span class="times">
|
<div class="times">
|
||||||
{#if entry}
|
{#each entries as entry (entry)}
|
||||||
{entry.is_24_7
|
<span class="time">
|
||||||
|
{entry.open_today_status
|
||||||
? "Open All Day"
|
? "Open All Day"
|
||||||
|
: entry.open_today_status === false
|
||||||
|
? "Closed"
|
||||||
: `${formatTime(entry.opens_at)} – ${formatTime(entry.closes_at)}`}
|
: `${formatTime(entry.opens_at)} – ${formatTime(entry.closes_at)}`}
|
||||||
{:else}
|
|
||||||
Closed
|
|
||||||
{/if}
|
|
||||||
</span>
|
</span>
|
||||||
|
{:else}
|
||||||
|
<span class="time">Not known</span>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<div class="subtitle">Directions:</div>
|
||||||
|
<p class="addrContainer">
|
||||||
|
{space.directions}
|
||||||
|
</p>
|
||||||
|
|
||||||
<div class="subtitle">Where it is:</div>
|
<div class="subtitle">Where it is:</div>
|
||||||
<p class="addrContainer">
|
<p class="addrContainer">
|
||||||
{#if place.name}
|
{#if place.name}
|
||||||
{place.name} <br />
|
{place.name} <br />
|
||||||
{/if}
|
{/if}
|
||||||
{#each place.formatted_address?.split(",") || [] as line (line)}
|
{#each place.formatted_address?.split(",") || [] as line, idx (line + idx)}
|
||||||
{line.trim()} <br />
|
{line.trim()} <br />
|
||||||
{/each}
|
{/each}
|
||||||
</p>
|
</p>
|
||||||
@@ -133,12 +193,15 @@
|
|||||||
isFeedbackPromptVisible = true;
|
isFeedbackPromptVisible = true;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Help categorise this space
|
Update Tags
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
{#if adminMode}
|
{#if adminMode}
|
||||||
|
<div class="buttonContainer">
|
||||||
<Button href="/space/{space.id}/edit" type="link">Edit</Button>
|
<Button href="/space/{space.id}/edit" type="link">Edit</Button>
|
||||||
|
<Button type="button" style="red" onclick={deleteSpace}>Delete</Button>
|
||||||
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<Button onclick={() => (isReportVisible = true)} style="red">Report</Button>
|
<Button onclick={() => (isReportVisible = true)} style="red">Report</Button>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -167,21 +230,23 @@
|
|||||||
background-color: #2e3c42;
|
background-color: #2e3c42;
|
||||||
width: 70%;
|
width: 70%;
|
||||||
border: none;
|
border: none;
|
||||||
margin: 0 auto;
|
margin: 1rem auto 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nameContainer {
|
.nameContainer {
|
||||||
z-index: 10;
|
display: flex;
|
||||||
display: block;
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.5rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0.6rem;
|
padding: 0.6rem;
|
||||||
margin-top: -0.5rem;
|
margin-top: -0.5rem;
|
||||||
object-position: center;
|
background-color: #189f5e;
|
||||||
background-color: #49bd85;
|
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
font-size: 2.8rem;
|
font-size: 2.8rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.descContainer {
|
.descContainer {
|
||||||
@@ -246,7 +311,7 @@
|
|||||||
padding: 0.7rem;
|
padding: 0.7rem;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
border: none;
|
border: none;
|
||||||
background-color: #49bd85;
|
background-color: #189f5e;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -260,22 +325,48 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.opening-entry {
|
.opening-entry {
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-columns: auto 1fr;
|
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
padding: 0.5rem 1.4rem;
|
padding: 0.5rem 1.4rem;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
background-color: #2e4653;
|
||||||
|
margin: 0.2rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
}
|
}
|
||||||
.opening-entry .day {
|
.opening-entry .day {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
white-space: nowrap;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
.opening-entry .times {
|
.opening-entry .times {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.25rem 1.5rem;
|
||||||
|
flex: 1;
|
||||||
|
align-items: end;
|
||||||
|
}
|
||||||
|
.opening-entry .time {
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
background-color: rgba(255, 255, 255, 0.1);
|
|
||||||
padding: 0.25rem 0.5rem;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
color: #eaffeb;
|
color: #eaffeb;
|
||||||
}
|
}
|
||||||
|
.imgContainer {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.title-fav {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
background: #189f5e;
|
||||||
|
border-radius: 0 0 0 0.5rem;
|
||||||
|
z-index: 1;
|
||||||
|
width: 3.75rem;
|
||||||
|
height: 3.75rem;
|
||||||
|
}
|
||||||
|
.buttonContainer {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ type StudySpaceData = Omit<
|
|||||||
day_of_week: number;
|
day_of_week: number;
|
||||||
opens_at: string;
|
opens_at: string;
|
||||||
closes_at: string;
|
closes_at: string;
|
||||||
is_24_7: boolean;
|
open_today_status: boolean | null;
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -21,6 +21,7 @@ export const load: PageServerLoad = async ({ params, locals: { supabase } }) =>
|
|||||||
return {
|
return {
|
||||||
space: {
|
space: {
|
||||||
description: "",
|
description: "",
|
||||||
|
directions: "",
|
||||||
building_location: undefined,
|
building_location: undefined,
|
||||||
location: "",
|
location: "",
|
||||||
tags: [],
|
tags: [],
|
||||||
@@ -42,7 +43,7 @@ export const load: PageServerLoad = async ({ params, locals: { supabase } }) =>
|
|||||||
const images = studySpaceData.study_space_images || [];
|
const images = studySpaceData.study_space_images || [];
|
||||||
const { data: hours, error: hoursErr } = await supabase
|
const { data: hours, error: hoursErr } = await supabase
|
||||||
.from("study_space_hours")
|
.from("study_space_hours")
|
||||||
.select("day_of_week, opens_at, closes_at, is_24_7")
|
.select("day_of_week, opens_at, closes_at, open_today_status")
|
||||||
.eq("study_space_id", params.id)
|
.eq("study_space_id", params.id)
|
||||||
.order("day_of_week", { ascending: true });
|
.order("day_of_week", { ascending: true });
|
||||||
if (hoursErr) error(500, "Failed to load opening times");
|
if (hoursErr) error(500, "Failed to load opening times");
|
||||||
|
|||||||
@@ -6,13 +6,16 @@
|
|||||||
import crossUrl from "$lib/assets/cross.svg";
|
import crossUrl from "$lib/assets/cross.svg";
|
||||||
import Button from "$lib/components/Button.svelte";
|
import Button from "$lib/components/Button.svelte";
|
||||||
import Images from "$lib/components/inputs/Images.svelte";
|
import Images from "$lib/components/inputs/Images.svelte";
|
||||||
|
import OpeningTimesDay from "$lib/components/inputs/OpeningTimesDay.svelte";
|
||||||
import {
|
import {
|
||||||
availableStudySpaceTags,
|
availableStudySpaceTags,
|
||||||
wifiTags,
|
wifiTags,
|
||||||
powerOutletTags,
|
powerOutletTags,
|
||||||
volumeTags,
|
volumeTags,
|
||||||
gmapsLoader,
|
gmapsLoader,
|
||||||
daysOfWeek
|
daysOfWeek,
|
||||||
|
timeToMins,
|
||||||
|
collectTimings
|
||||||
} from "$lib";
|
} from "$lib";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import type { Json } from "$lib/database.js";
|
import type { Json } from "$lib/database.js";
|
||||||
@@ -21,23 +24,22 @@
|
|||||||
const { supabase } = $derived(data);
|
const { supabase } = $derived(data);
|
||||||
|
|
||||||
const { space, images } = $derived(data);
|
const { space, images } = $derived(data);
|
||||||
|
|
||||||
|
interface OpeningTime {
|
||||||
|
day_of_week: number;
|
||||||
|
opens_at: string;
|
||||||
|
closes_at: string;
|
||||||
|
open_today_status: boolean | null;
|
||||||
|
}
|
||||||
const studySpaceData = $state({
|
const studySpaceData = $state({
|
||||||
opening_times: daysOfWeek.map((_, index) => ({
|
opening_times: [] as OpeningTime[],
|
||||||
day_of_week: index,
|
|
||||||
opens_at: "",
|
|
||||||
closes_at: "",
|
|
||||||
is_24_7: false
|
|
||||||
})),
|
|
||||||
...space
|
...space
|
||||||
});
|
});
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (!space) return;
|
if (!space) return;
|
||||||
const { opening_times, ...rest } = space;
|
Object.assign(studySpaceData, space);
|
||||||
Object.assign(studySpaceData, rest);
|
studySpaceData.opening_times = space.opening_times ?? [];
|
||||||
if (opening_times) {
|
|
||||||
studySpaceData.opening_times = opening_times;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let scrollPosition = $state(0);
|
let scrollPosition = $state(0);
|
||||||
@@ -57,12 +59,96 @@
|
|||||||
);
|
);
|
||||||
let spaceImgs = $state<FileList>();
|
let spaceImgs = $state<FileList>();
|
||||||
let uploading = $state(false);
|
let uploading = $state(false);
|
||||||
|
function checkTimings() {
|
||||||
|
let cannotExist = [] as number[];
|
||||||
|
|
||||||
|
let hasAllDays = Object.values(collectTimings(studySpaceData.opening_times)).every(
|
||||||
|
(item) => !(Array.isArray(item) && item.length === 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
(allDays.closes_at === "" || allDays.opens_at === "") &&
|
||||||
|
allDays.open_today_status === null &&
|
||||||
|
studySpaceData.opening_times.length > 0 &&
|
||||||
|
!hasAllDays
|
||||||
|
) {
|
||||||
|
alert(`No opening time provided for all other days.`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const opensAtMinsAll = timeToMins(allDays.opens_at);
|
||||||
|
const closesAtMinsAll = timeToMins(allDays.closes_at);
|
||||||
|
if (opensAtMinsAll >= closesAtMinsAll) {
|
||||||
|
alert(`Opening time for all days is after closing time.`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const entry of studySpaceData.opening_times) {
|
||||||
|
if (cannotExist.includes(entry.day_of_week)) {
|
||||||
|
alert(
|
||||||
|
"You marked a day as either closed or open all day, and then provided another timing."
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (entry.open_today_status != null) {
|
||||||
|
cannotExist.push(entry.day_of_week);
|
||||||
|
}
|
||||||
|
const opensAtMins = timeToMins(entry.opens_at);
|
||||||
|
const closesAtMins = timeToMins(entry.closes_at);
|
||||||
|
if (opensAtMins >= closesAtMins) {
|
||||||
|
alert(`Opening time for ${daysOfWeek[entry.day_of_week]} is after closing time.`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function genTimings(studySpaceId: string) {
|
||||||
|
const fullDayOfWeek = [0, 1, 2, 3, 4, 5, 6];
|
||||||
|
// all day only
|
||||||
|
if (
|
||||||
|
studySpaceData.opening_times.length === 0 &&
|
||||||
|
((allDays.closes_at != "" && allDays.opens_at != "") ||
|
||||||
|
allDays.open_today_status != null)
|
||||||
|
) {
|
||||||
|
return fullDayOfWeek.map((day) => ({
|
||||||
|
study_space_id: studySpaceId,
|
||||||
|
day_of_week: day,
|
||||||
|
opens_at: allDays.open_today_status === null ? allDays.opens_at : "00:00",
|
||||||
|
closes_at: allDays.open_today_status === null ? allDays.closes_at : "00:01",
|
||||||
|
open_today_status: allDays.open_today_status
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
// some days specified
|
||||||
|
const nonDefinedDays = fullDayOfWeek.filter(
|
||||||
|
(day) => !new Set(studySpaceData.opening_times.map((h) => h.day_of_week)).has(day)
|
||||||
|
);
|
||||||
|
return studySpaceData.opening_times
|
||||||
|
.map((h) => ({
|
||||||
|
study_space_id: studySpaceId,
|
||||||
|
day_of_week: h.day_of_week,
|
||||||
|
opens_at: h.open_today_status === null ? h.opens_at : "00:00",
|
||||||
|
closes_at: h.open_today_status === null ? h.closes_at : "00:01",
|
||||||
|
open_today_status: h.open_today_status
|
||||||
|
}))
|
||||||
|
.concat(
|
||||||
|
nonDefinedDays.map((day) => ({
|
||||||
|
study_space_id: studySpaceId,
|
||||||
|
day_of_week: day,
|
||||||
|
opens_at: allDays.open_today_status === null ? allDays.opens_at : "00:00",
|
||||||
|
closes_at: allDays.open_today_status === null ? allDays.closes_at : "00:01",
|
||||||
|
open_today_status: allDays.open_today_status
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async function uploadStudySpace() {
|
async function uploadStudySpace() {
|
||||||
|
if (!checkTimings()) return;
|
||||||
if (!spaceImgs || spaceImgs.length < 1) return alert("Please select an image file.");
|
if (!spaceImgs || spaceImgs.length < 1) return alert("Please select an image file.");
|
||||||
if (!studySpaceData.building_location) return alert("Please select a building location.");
|
if (!studySpaceData.building_location) return alert("Please select a building location.");
|
||||||
|
|
||||||
const { opening_times, ...spacePayload } = studySpaceData;
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const { opening_times: _, ...spacePayload } = studySpaceData;
|
||||||
|
|
||||||
const { data: studySpaceInsert, error: studySpaceError } = await supabase
|
const { data: studySpaceInsert, error: studySpaceError } = await supabase
|
||||||
.from("study_spaces")
|
.from("study_spaces")
|
||||||
@@ -126,17 +212,24 @@
|
|||||||
.eq("study_space_id", studySpaceInsert.id);
|
.eq("study_space_id", studySpaceInsert.id);
|
||||||
if (deleteErr) return alert(`Error clearing old hours: ${deleteErr.message}`);
|
if (deleteErr) return alert(`Error clearing old hours: ${deleteErr.message}`);
|
||||||
|
|
||||||
const { error: hoursErr } = await supabase.from("study_space_hours").insert(
|
// Nothing is provided
|
||||||
opening_times.map((h) => ({
|
if (
|
||||||
study_space_id: studySpaceInsert.id,
|
(allDays.closes_at != "" && allDays.opens_at != "") ||
|
||||||
day_of_week: h.day_of_week,
|
studySpaceData.opening_times.length === 7 ||
|
||||||
opens_at: h.opens_at,
|
allDays.open_today_status != null
|
||||||
closes_at: h.closes_at,
|
) {
|
||||||
is_24_7: h.is_24_7
|
const { error: hoursErr } = await supabase
|
||||||
}))
|
.from("study_space_hours")
|
||||||
);
|
.insert(genTimings(studySpaceInsert.id));
|
||||||
if (hoursErr) return alert(`Error saving opening times: ${hoursErr.message}`);
|
if (hoursErr) return alert(`Error saving opening times: ${hoursErr.message}`);
|
||||||
|
}
|
||||||
|
await supabase.channel("study_space_updates").send({
|
||||||
|
type: "broadcast",
|
||||||
|
event: "study_space_updated",
|
||||||
|
payload: {
|
||||||
|
study_space_id: studySpaceInsert.id
|
||||||
|
}
|
||||||
|
});
|
||||||
alert("Thank you for your contribution!");
|
alert("Thank you for your contribution!");
|
||||||
// Redirect to the new study space page
|
// Redirect to the new study space page
|
||||||
await goto(`/space/${studySpaceInsert.id}`, {
|
await goto(`/space/${studySpaceInsert.id}`, {
|
||||||
@@ -196,20 +289,12 @@
|
|||||||
});
|
});
|
||||||
spaceImgs = dt.files;
|
spaceImgs = dt.files;
|
||||||
});
|
});
|
||||||
|
// Opening times
|
||||||
// --- Helper functions for opening times ---
|
let allDays = $state({
|
||||||
function toggle247(index: number) {
|
opens_at: "",
|
||||||
const ot = studySpaceData.opening_times[index];
|
closes_at: "",
|
||||||
if (ot.is_24_7) {
|
open_today_status: null
|
||||||
ot.opens_at = "00:00";
|
});
|
||||||
ot.closes_at = "00:00";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateTimes(index: number) {
|
|
||||||
const ot = studySpaceData.opening_times[index];
|
|
||||||
ot.is_24_7 = ot.opens_at === "00:00" && ot.closes_at === "00:00";
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Navbar>
|
<Navbar>
|
||||||
@@ -265,10 +350,18 @@
|
|||||||
<Text
|
<Text
|
||||||
name="location"
|
name="location"
|
||||||
bind:value={studySpaceData.location}
|
bind:value={studySpaceData.location}
|
||||||
placeholder="Room 123, Floor 1"
|
placeholder="Huxeley Labs 225"
|
||||||
|
maxlength={35}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{#if (studySpaceData.location ?? "").length > 25}
|
||||||
|
<p class="lengthPopup">
|
||||||
|
Try to keep the name succinct—for example, building + room name. Put any further
|
||||||
|
information like floor number in the description.
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="compulsoryTags">
|
<div class="compulsoryTags">
|
||||||
<div class="compulsoryContainer">
|
<div class="compulsoryContainer">
|
||||||
<label for="volume">Sound level:</label>
|
<label for="volume">Sound level:</label>
|
||||||
@@ -302,41 +395,43 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<label for="openingTimes">Opening times (Optional):</label>
|
||||||
<label for="opening-times-label">Opening Times:</label>
|
<div class="allDaysTiming">
|
||||||
<div class="opening-times">
|
{#each studySpaceData.opening_times as opening_time, index (opening_time)}
|
||||||
{#each daysOfWeek as day, index (index)}
|
<OpeningTimesDay
|
||||||
<div class="opening-time-item">
|
{index}
|
||||||
<label for={"opens-" + index}>{day}</label>
|
bind:openingValue={opening_time.opens_at}
|
||||||
<input
|
bind:closingValue={opening_time.closes_at}
|
||||||
id={"opens-" + index}
|
bind:openTodayStatus={opening_time.open_today_status}
|
||||||
type="time"
|
bind:day={opening_time.day_of_week}
|
||||||
bind:value={studySpaceData.opening_times[index].opens_at}
|
onHide={() => {
|
||||||
required
|
studySpaceData.opening_times.splice(index, 1);
|
||||||
onchange={() => updateTimes(index)}
|
}}
|
||||||
/>
|
/>
|
||||||
<span>to</span>
|
<hr />
|
||||||
<input
|
|
||||||
id={"closes-" + index}
|
|
||||||
type="time"
|
|
||||||
bind:value={studySpaceData.opening_times[index].closes_at}
|
|
||||||
required
|
|
||||||
onchange={() => updateTimes(index)}
|
|
||||||
/>
|
|
||||||
<label for={"is247-" + index}>
|
|
||||||
<input
|
|
||||||
id={"is247-" + index}
|
|
||||||
type="checkbox"
|
|
||||||
bind:checked={studySpaceData.opening_times[index].is_24_7}
|
|
||||||
onchange={() => toggle247(index)}
|
|
||||||
/>
|
|
||||||
All day
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
{/each}
|
{/each}
|
||||||
|
<OpeningTimesDay
|
||||||
|
index={-1}
|
||||||
|
bind:openingValue={allDays.opens_at}
|
||||||
|
bind:closingValue={allDays.closes_at}
|
||||||
|
bind:openTodayStatus={allDays.open_today_status}
|
||||||
|
day={studySpaceData.opening_times.length === 0 ? 7 : 8}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<Button
|
||||||
|
style="normal"
|
||||||
|
type="button"
|
||||||
|
onclick={() => {
|
||||||
|
studySpaceData.opening_times.push({
|
||||||
|
day_of_week: 0,
|
||||||
|
opens_at: "09:00",
|
||||||
|
closes_at: "17:00",
|
||||||
|
open_today_status: null
|
||||||
|
});
|
||||||
|
}}>Add new day</Button
|
||||||
|
>
|
||||||
|
|
||||||
<label for="tags">Additional tags:</label>
|
<label for="tags">Additional tags (Optional):</label>
|
||||||
<div class="tagDisplay">
|
<div class="tagDisplay">
|
||||||
{#each studySpaceData.tags as tagName (tagName)}
|
{#each studySpaceData.tags as tagName (tagName)}
|
||||||
<button class="tag" onclick={deleteTag(tagName)} type="button">
|
<button class="tag" onclick={deleteTag(tagName)} type="button">
|
||||||
@@ -385,7 +480,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<label for="description">Optional brief description:</label>
|
<label for="description">Brief description (Optional):</label>
|
||||||
<Textarea
|
<Textarea
|
||||||
name="description"
|
name="description"
|
||||||
bind:value={studySpaceData.description}
|
bind:value={studySpaceData.description}
|
||||||
@@ -393,6 +488,14 @@
|
|||||||
rows={2}
|
rows={2}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<label for="directions">Give directions:</label>
|
||||||
|
<Textarea
|
||||||
|
name="directions"
|
||||||
|
bind:value={studySpaceData.directions}
|
||||||
|
placeholder="Turn left once you enter Huxley and walk straight."
|
||||||
|
rows={2}
|
||||||
|
/>
|
||||||
|
|
||||||
<label for="building-location">Add the building location:</label>
|
<label for="building-location">Add the building location:</label>
|
||||||
<Text
|
<Text
|
||||||
name="building-location"
|
name="building-location"
|
||||||
@@ -557,7 +660,7 @@
|
|||||||
.additionalImages {
|
.additionalImages {
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
background: linear-gradient(-83deg, #3fb095, #49bd85);
|
background: linear-gradient(-83deg, #3fb095, #189f5e);
|
||||||
box-shadow: 0rem 0rem 0.5rem #182125;
|
box-shadow: 0rem 0rem 0.5rem #182125;
|
||||||
color: #eaffeb;
|
color: #eaffeb;
|
||||||
border: none;
|
border: none;
|
||||||
@@ -572,34 +675,28 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Opening times layout and inputs styling */
|
/* Opening times layout and inputs styling */
|
||||||
.opening-times {
|
.allDaysTiming {
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
background: none;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
flex-wrap: wrap;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.opening-time-item {
|
hr {
|
||||||
display: flex;
|
margin: 0%;
|
||||||
align-items: center;
|
padding: 0;
|
||||||
gap: 0.5rem;
|
width: 100%;
|
||||||
|
height: 2px;
|
||||||
|
border: none;
|
||||||
|
background-color: #eaffeb;
|
||||||
|
border-radius: 5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.opening-time-item label {
|
.lengthPopup {
|
||||||
margin-top: 0;
|
background-color: #2e4653;
|
||||||
width: 6rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.opening-time-item input[type="time"] {
|
|
||||||
padding: 0.5rem;
|
|
||||||
height: 2.5rem;
|
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
border: 2px solid #eaffeb;
|
padding: 0.5rem;
|
||||||
background: none;
|
|
||||||
color: #eaffeb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.opening-time-item span {
|
|
||||||
margin: 0 0.5rem;
|
|
||||||
color: #eaffeb;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
const { data } = $props();
|
const { data } = $props();
|
||||||
const { reports, supabase } = $derived(data);
|
const { reports, supabase } = $derived(data);
|
||||||
import { invalidate } from "$app/navigation";
|
import { invalidate } from "$app/navigation";
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
|
||||||
let deleting = $state(false);
|
let deleting = $state(false);
|
||||||
|
|
||||||
@@ -18,6 +19,16 @@
|
|||||||
return alert(`Error submitting report: ${reportDeleteError.message}`);
|
return alert(`Error submitting report: ${reportDeleteError.message}`);
|
||||||
else alert("Report deleted successfully!");
|
else alert("Report deleted successfully!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
const reportsChannel = supabase
|
||||||
|
.channel("report_updates")
|
||||||
|
.on("broadcast", { event: "reports_updated" }, () => {
|
||||||
|
invalidate("db:reports");
|
||||||
|
})
|
||||||
|
.subscribe();
|
||||||
|
return () => reportsChannel.unsubscribe();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Navbar>
|
<Navbar>
|
||||||
@@ -99,7 +110,7 @@
|
|||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
border: none;
|
border: none;
|
||||||
background-color: #49bd85;
|
background-color: #189f5e;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|||||||
@@ -2,3 +2,4 @@ ALTER TABLE study_spaces DROP COLUMN title;
|
|||||||
ALTER TABLE study_spaces ADD COLUMN building_address text;
|
ALTER TABLE study_spaces ADD COLUMN building_address text;
|
||||||
ALTER TABLE study_spaces ADD COLUMN description text;
|
ALTER TABLE study_spaces ADD COLUMN description text;
|
||||||
ALTER TABLE study_spaces ADD COLUMN location text;
|
ALTER TABLE study_spaces ADD COLUMN location text;
|
||||||
|
ALTER TABLE study_spaces ADD COLUMN directions text;
|
||||||
|
|||||||
@@ -26,3 +26,4 @@ $$;
|
|||||||
CREATE TRIGGER users_handle_new_user
|
CREATE TRIGGER users_handle_new_user
|
||||||
AFTER INSERT ON auth.users
|
AFTER INSERT ON auth.users
|
||||||
FOR EACH ROW EXECUTE FUNCTION handle_new_user();
|
FOR EACH ROW EXECUTE FUNCTION handle_new_user();
|
||||||
|
|
||||||
|
|||||||
5
supabase/migrations/20250612170906_renamed-247.sql
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
alter table "public"."study_space_hours" drop column "is_24_7";
|
||||||
|
|
||||||
|
alter table "public"."study_space_hours" add column "open_today_status" boolean;
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
alter table "public"."study_space_hours" alter column "day_of_week" set not null;
|
||||||
|
|
||||||
|
|
||||||
12
supabase/migrations/20250613105939_favourites.sql
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
-- Table to store users' favourite study spaces
|
||||||
|
CREATE TABLE favourite_study_spaces (
|
||||||
|
user_id uuid REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
study_space_id uuid REFERENCES study_spaces(id) ON DELETE CASCADE,
|
||||||
|
created_at timestamp with time zone DEFAULT now(),
|
||||||
|
updated_at timestamp with time zone DEFAULT now(),
|
||||||
|
PRIMARY KEY (user_id, study_space_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TRIGGER favourite_study_spaces_updated_at
|
||||||
|
AFTER UPDATE ON favourite_study_spaces
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION handle_updated_at();
|
||||||
1
supabase/migrations/20250613133130_directions.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE study_spaces ADD COLUMN IF NOT EXISTS directions text;
|
||||||
@@ -9,6 +9,7 @@ CREATE POLICY "Whack"
|
|||||||
CREATE TABLE study_spaces (
|
CREATE TABLE study_spaces (
|
||||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
description text,
|
description text,
|
||||||
|
directions text,
|
||||||
-- Location within building, e.g., "Room 101"
|
-- Location within building, e.g., "Room 101"
|
||||||
location text,
|
location text,
|
||||||
-- Not bothered to write a proper data migration
|
-- Not bothered to write a proper data migration
|
||||||
@@ -43,10 +44,10 @@ CREATE TABLE reports (
|
|||||||
CREATE TABLE study_space_hours (
|
CREATE TABLE study_space_hours (
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
study_space_id UUID REFERENCES study_spaces(id) ON DELETE CASCADE,
|
study_space_id UUID REFERENCES study_spaces(id) ON DELETE CASCADE,
|
||||||
day_of_week INT CHECK (day_of_week BETWEEN 0 AND 6), -- 0 = Sunday, 6 = Saturday
|
day_of_week INT CHECK (day_of_week BETWEEN 0 AND 6) NOT NULL, -- 0 = Sunday, 6 = Saturday
|
||||||
opens_at TIME NOT NULL,
|
opens_at TIME NOT NULL,
|
||||||
closes_at TIME NOT NULL,
|
closes_at TIME NOT NULL,
|
||||||
is_24_7 BOOLEAN DEFAULT FALSE,
|
open_today_status BOOLEAN,
|
||||||
created_at timestamp with time zone DEFAULT now(),
|
created_at timestamp with time zone DEFAULT now(),
|
||||||
updated_at timestamp with time zone DEFAULT now()
|
updated_at timestamp with time zone DEFAULT now()
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ CREATE TABLE users (
|
|||||||
id uuid PRIMARY KEY REFERENCES auth.users ON DELETE CASCADE,
|
id uuid PRIMARY KEY REFERENCES auth.users ON DELETE CASCADE,
|
||||||
is_admin boolean NOT NULL DEFAULT false,
|
is_admin boolean NOT NULL DEFAULT false,
|
||||||
created_at timestamp with time zone NOT NULL DEFAULT now(),
|
created_at timestamp with time zone NOT NULL DEFAULT now(),
|
||||||
updated_at timestamp with time zone NOT NULL DEFAULT now(),
|
updated_at timestamp with time zone NOT NULL DEFAULT now()
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TRIGGER users_handle_updated_at
|
CREATE TRIGGER users_handle_updated_at
|
||||||
@@ -26,3 +26,16 @@ $$;
|
|||||||
CREATE TRIGGER users_handle_new_user
|
CREATE TRIGGER users_handle_new_user
|
||||||
AFTER INSERT ON auth.users
|
AFTER INSERT ON auth.users
|
||||||
FOR EACH ROW EXECUTE FUNCTION handle_new_user();
|
FOR EACH ROW EXECUTE FUNCTION handle_new_user();
|
||||||
|
|
||||||
|
-- Table to store users' favourite study spaces
|
||||||
|
CREATE TABLE favourite_study_spaces (
|
||||||
|
user_id uuid REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
study_space_id uuid REFERENCES study_spaces(id) ON DELETE CASCADE,
|
||||||
|
created_at timestamp with time zone DEFAULT now(),
|
||||||
|
updated_at timestamp with time zone DEFAULT now(),
|
||||||
|
PRIMARY KEY (user_id, study_space_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TRIGGER favourite_study_spaces_updated_at
|
||||||
|
AFTER UPDATE ON favourite_study_spaces
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION handle_updated_at();
|
||||||
|
|||||||