diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d23fb38..8bb2c1c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,22 +11,34 @@ importers: '@apollo/client': specifier: ^4.3.2 version: 4.3.2(graphql@17.0.2)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(rxjs@7.8.2) + dotenv: + specifier: ^18.0.6 + version: 18.0.7 graphql: specifier: ^17.0.2 version: 17.0.2 + pg: + specifier: ^8.23.1 + version: 8.23.1 react: specifier: ^19.2.8 version: 19.3.0 react-dom: specifier: ^19.2.8 version: 19.3.0(react@19.3.0) + react-router-dom: + specifier: ^7.18.4 + version: 7.18.4(react-dom@19.3.0(react@19.3.0))(react@19.3.0) devDependencies: '@eslint/js': specifier: ^10.0.1 version: 10.0.1(eslint@10.11.0) '@types/node': - specifier: ^24.13.3 - version: 24.19.0 + specifier: ^24.19.1 + version: 24.19.2 + '@types/pg': + specifier: ^8.23.1 + version: 8.23.1 '@types/react': specifier: ^19.3.0 version: 19.3.0 @@ -35,7 +47,7 @@ importers: version: 19.3.0(@types/react@19.3.0) '@vitejs/plugin-react': specifier: ^6.1.1 - version: 6.1.1(vite@8.3.1(@types/node@24.19.0)) + version: 6.1.1(vite@8.3.1(@types/node@24.19.2)(esbuild@0.28.2)(tsx@4.23.15)) eslint: specifier: ^10.10.0 version: 10.11.0 @@ -48,6 +60,9 @@ importers: globals: specifier: ^17.12.0 version: 17.13.0 + tsx: + specifier: ^4.23.15 + version: 4.23.15 typescript: specifier: ~6.0.2 version: 6.0.3 @@ -56,7 +71,7 @@ importers: version: 8.71.0(eslint@10.11.0)(typescript@6.0.3) vite: specifier: ^8.3.0 - version: 8.3.1(@types/node@24.19.0) + version: 8.3.1(@types/node@24.19.2)(esbuild@0.28.2)(tsx@4.23.15) packages: @@ -152,6 +167,162 @@ packages: '@cacheable/utils@2.5.0': resolution: {integrity: sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==} + '@esbuild/aix-ppc64@0.28.2': + resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.28.2': + resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.28.2': + resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.28.2': + resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.28.2': + resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.2': + resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.28.2': + resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.2': + resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.28.2': + resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.28.2': + resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.28.2': + resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.28.2': + resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.28.2': + resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.28.2': + resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.2': + resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.28.2': + resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.28.2': + resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.2': + resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.2': + resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.2': + resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.2': + resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.2': + resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.28.2': + resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.28.2': + resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.28.2': + resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.28.2': + resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-utils@4.10.1': resolution: {integrity: sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -355,6 +526,12 @@ packages: '@types/node@24.19.0': resolution: {integrity: sha512-zY+5tKxXdhGh1PYI0ac+7juvEu4OI6vWtVVoj5i2m42jxAY1U+zHGt6QCyOFwykdP62sM3MJ9stoYYUw5aCWew==} + '@types/node@24.19.2': + resolution: {integrity: sha512-93d49wCWJkCyN87MxLpNN6KXPCwFw7bVUg79mGUV1OS40JGWwVCr8n61DXCSqz7CV6/VfPkb7xec7uP2M+QHWQ==} + + '@types/pg@8.23.1': + resolution: {integrity: sha512-fKVHpikPdg4GKks3JuLEhvwSyvwzF23hnabPy6DD8ljVbC7+6J5dQzdv4arV6jqq57djnMgs1HKBxX4P8aBI3A==} + '@types/react-dom@19.3.0': resolution: {integrity: sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==} peerDependencies: @@ -494,6 +671,10 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -517,9 +698,19 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} + dotenv@18.0.7: + resolution: {integrity: sha512-QEvMEF8ca3GNZJJnAbOgxpj5ULhBvnMcURp+XKrtvqFNPgjBvYoswQ0Jl2cO7JzCjxZjJhr9ZuIEhlrRxMft/Q==} + engines: {node: '>=12'} + hasBin: true + electron-to-chromium@1.5.443: resolution: {integrity: sha512-TDJG36L9A3CWWwZ97HKaE+Iz1sW80pNp6sAU0owzCIV3Zc9eVyRqZK8Vz6NKuMIU8WjP0pIkoqCa0VRf/yFD0Q==} + esbuild@0.28.2: + resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -827,6 +1018,40 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + pg-cloudflare@1.4.1: + resolution: {integrity: sha512-6PQbsFWZcp9EmJEwy5cGQ2La+AMWpP46lgbb8X+U/XsHIUweYDNCpeuKck5RxL2MdVFi7krbbEi5nX4Zh7JhrQ==} + + pg-connection-string@2.14.1: + resolution: {integrity: sha512-qR3kGNPBLpCNtz0evbKA0Y/MRFXwSSdT+pTJvYp/bXTcReZbvX1kzF0IyTc1QnxqF7AZbOeBhNL8R5mYQZV/MA==} + + pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + pg-pool@3.14.0: + resolution: {integrity: sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==} + peerDependencies: + pg: '>=8.0' + + pg-protocol@1.16.1: + resolution: {integrity: sha512-p9VOFMiHB/ZbJATetbg+99PxssTVSQRnyuPSQ67mN1+1KBOjZaZ83ZQzltnxPhJwSsC3nwVjJ10DVJlerbFzLg==} + + pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + + pg@8.23.1: + resolution: {integrity: sha512-aL96AHANtWjPLDOLqnhx+ngp9+UK7ETEU8VJrDCGvsSSi/mGLcWYsS6Herg7lmaBJe4uwrfqsa7gTEFaSizDoQ==} + engines: {node: '>= 16.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + + pgpass@1.0.5: + resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -838,6 +1063,22 @@ packages: resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==} engines: {node: ^10 || ^12 || >=14} + postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + postgres-bytea@1.0.1: + resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==} + engines: {node: '>=0.10.0'} + + postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -855,6 +1096,23 @@ packages: peerDependencies: react: ^19.3.0 + react-router-dom@7.18.4: + resolution: {integrity: sha512-yrfmJHIpDG7taCpqKjT1G5B6q3O2K+RN8/fgNf0lTjCwiPbQ0ei6vXX9ZjQR+7ld8Tr7Z5xmyMnZ8YJrphWQUw==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + + react-router@7.18.4: + resolution: {integrity: sha512-PUPQcMhMGRAslLcvtlPz/kmzBEWPhLdgLFrL7pLNepBL6dX0lWj4WD2cUYVgYCuT3jxvghYFg81cDTj44DhetQ==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + peerDependenciesMeta: + react-dom: + optional: true + react@19.3.0: resolution: {integrity: sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==} engines: {node: '>=0.10.0'} @@ -879,6 +1137,9 @@ packages: engines: {node: '>=10'} hasBin: true + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -891,6 +1152,10 @@ packages: resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==} engines: {node: '>=0.10.0'} + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + tinyglobby@0.2.17: resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} @@ -904,6 +1169,11 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tsx@4.23.15: + resolution: {integrity: sha512-Yiex1Ovn8z2xPpOWckIiysV1SSyRMY9BkLF++q0yKiDxCqRhosKfMg3janKkiLBwZ5c/YryloKwGZcrEmtwxKw==} + engines: {node: '>=18.0.0'} + hasBin: true + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -984,6 +1254,10 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} @@ -1129,6 +1403,84 @@ snapshots: hashery: 1.5.1 keyv: 5.6.0 + '@esbuild/aix-ppc64@0.28.2': + optional: true + + '@esbuild/android-arm64@0.28.2': + optional: true + + '@esbuild/android-arm@0.28.2': + optional: true + + '@esbuild/android-x64@0.28.2': + optional: true + + '@esbuild/darwin-arm64@0.28.2': + optional: true + + '@esbuild/darwin-x64@0.28.2': + optional: true + + '@esbuild/freebsd-arm64@0.28.2': + optional: true + + '@esbuild/freebsd-x64@0.28.2': + optional: true + + '@esbuild/linux-arm64@0.28.2': + optional: true + + '@esbuild/linux-arm@0.28.2': + optional: true + + '@esbuild/linux-ia32@0.28.2': + optional: true + + '@esbuild/linux-loong64@0.28.2': + optional: true + + '@esbuild/linux-mips64el@0.28.2': + optional: true + + '@esbuild/linux-ppc64@0.28.2': + optional: true + + '@esbuild/linux-riscv64@0.28.2': + optional: true + + '@esbuild/linux-s390x@0.28.2': + optional: true + + '@esbuild/linux-x64@0.28.2': + optional: true + + '@esbuild/netbsd-arm64@0.28.2': + optional: true + + '@esbuild/netbsd-x64@0.28.2': + optional: true + + '@esbuild/openbsd-arm64@0.28.2': + optional: true + + '@esbuild/openbsd-x64@0.28.2': + optional: true + + '@esbuild/openharmony-arm64@0.28.2': + optional: true + + '@esbuild/sunos-x64@0.28.2': + optional: true + + '@esbuild/win32-arm64@0.28.2': + optional: true + + '@esbuild/win32-ia32@0.28.2': + optional: true + + '@esbuild/win32-x64@0.28.2': + optional: true + '@eslint-community/eslint-utils@4.10.1(eslint@10.11.0)': dependencies: eslint: 10.11.0 @@ -1269,6 +1621,16 @@ snapshots: dependencies: undici-types: 7.24.6 + '@types/node@24.19.2': + dependencies: + undici-types: 7.24.6 + + '@types/pg@8.23.1': + dependencies: + '@types/node': 24.19.0 + pg-protocol: 1.16.1 + pg-types: 2.2.0 + '@types/react-dom@19.3.0(@types/react@19.3.0)': dependencies: '@types/react': 19.3.0 @@ -1368,10 +1730,10 @@ snapshots: '@typescript-eslint/types': 8.71.0 eslint-visitor-keys: 5.0.1 - '@vitejs/plugin-react@6.1.1(vite@8.3.1(@types/node@24.19.0))': + '@vitejs/plugin-react@6.1.1(vite@8.3.1(@types/node@24.19.2)(esbuild@0.28.2)(tsx@4.23.15))': dependencies: '@rolldown/pluginutils': 1.0.1 - vite: 8.3.1(@types/node@24.19.0) + vite: 8.3.1(@types/node@24.19.2)(esbuild@0.28.2)(tsx@4.23.15) '@wry/caches@1.0.1': dependencies: @@ -1430,6 +1792,8 @@ snapshots: convert-source-map@2.0.0: {} + cookie@1.1.1: {} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -1446,8 +1810,39 @@ snapshots: detect-libc@2.1.2: {} + dotenv@18.0.7: {} + electron-to-chromium@1.5.443: {} + esbuild@0.28.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.2 + '@esbuild/android-arm': 0.28.2 + '@esbuild/android-arm64': 0.28.2 + '@esbuild/android-x64': 0.28.2 + '@esbuild/darwin-arm64': 0.28.2 + '@esbuild/darwin-x64': 0.28.2 + '@esbuild/freebsd-arm64': 0.28.2 + '@esbuild/freebsd-x64': 0.28.2 + '@esbuild/linux-arm': 0.28.2 + '@esbuild/linux-arm64': 0.28.2 + '@esbuild/linux-ia32': 0.28.2 + '@esbuild/linux-loong64': 0.28.2 + '@esbuild/linux-mips64el': 0.28.2 + '@esbuild/linux-ppc64': 0.28.2 + '@esbuild/linux-riscv64': 0.28.2 + '@esbuild/linux-s390x': 0.28.2 + '@esbuild/linux-x64': 0.28.2 + '@esbuild/netbsd-arm64': 0.28.2 + '@esbuild/netbsd-x64': 0.28.2 + '@esbuild/openbsd-arm64': 0.28.2 + '@esbuild/openbsd-x64': 0.28.2 + '@esbuild/openharmony-arm64': 0.28.2 + '@esbuild/sunos-x64': 0.28.2 + '@esbuild/win32-arm64': 0.28.2 + '@esbuild/win32-ia32': 0.28.2 + '@esbuild/win32-x64': 0.28.2 + escalade@3.2.0: {} escape-string-regexp@4.0.0: {} @@ -1720,6 +2115,41 @@ snapshots: path-key@3.1.1: {} + pg-cloudflare@1.4.1: + optional: true + + pg-connection-string@2.14.1: {} + + pg-int8@1.0.1: {} + + pg-pool@3.14.0(pg@8.23.1): + dependencies: + pg: 8.23.1 + + pg-protocol@1.16.1: {} + + pg-types@2.2.0: + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.1 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + + pg@8.23.1: + dependencies: + pg-connection-string: 2.14.1 + pg-pool: 3.14.0(pg@8.23.1) + pg-protocol: 1.16.1 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.4.1 + + pgpass@1.0.5: + dependencies: + split2: 4.2.0 + picocolors@1.1.1: {} picomatch@4.0.7: {} @@ -1730,6 +2160,16 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.2 + postgres-array@2.0.0: {} + + postgres-bytea@1.0.1: {} + + postgres-date@1.0.7: {} + + postgres-interval@1.2.0: + dependencies: + xtend: 4.0.2 + prelude-ls@1.2.1: {} punycode@2.3.1: {} @@ -1743,6 +2183,20 @@ snapshots: react: 19.3.0 scheduler: 0.28.0 + react-router-dom@7.18.4(react-dom@19.3.0(react@19.3.0))(react@19.3.0): + dependencies: + react: 19.3.0 + react-dom: 19.3.0(react@19.3.0) + react-router: 7.18.4(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + + react-router@7.18.4(react-dom@19.3.0(react@19.3.0))(react@19.3.0): + dependencies: + cookie: 1.1.1 + react: 19.3.0 + set-cookie-parser: 2.7.2 + optionalDependencies: + react-dom: 19.3.0(react@19.3.0) + react@19.3.0: {} rolldown@1.2.12: @@ -1776,6 +2230,8 @@ snapshots: semver@7.8.5: {} + set-cookie-parser@2.7.2: {} + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -1784,6 +2240,8 @@ snapshots: source-map-js@1.2.2: {} + split2@4.2.0: {} + tinyglobby@0.2.17: dependencies: fdir: 6.5.0(picomatch@4.0.7) @@ -1795,6 +2253,12 @@ snapshots: tslib@2.8.1: {} + tsx@4.23.15: + dependencies: + esbuild: 0.28.2 + optionalDependencies: + fsevents: 2.3.3 + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 @@ -1824,7 +2288,7 @@ snapshots: dependencies: punycode: 2.3.1 - vite@8.3.1(@types/node@24.19.0): + vite@8.3.1(@types/node@24.19.2)(esbuild@0.28.2)(tsx@4.23.15): dependencies: lightningcss: 1.33.0 picomatch: 4.0.7 @@ -1832,8 +2296,10 @@ snapshots: rolldown: 1.2.12 tinyglobby: 0.2.17 optionalDependencies: - '@types/node': 24.19.0 + '@types/node': 24.19.2 + esbuild: 0.28.2 fsevents: 2.3.3 + tsx: 4.23.15 which@2.0.2: dependencies: @@ -1841,6 +2307,8 @@ snapshots: word-wrap@1.2.5: {} + xtend@4.0.2: {} + yallist@3.1.1: {} yocto-queue@0.1.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..5ed0b5a --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +allowBuilds: + esbuild: true diff --git a/public/fonts/ClashDisplay-Regular.woff2 b/public/fonts/ClashDisplay-Regular.woff2 new file mode 100644 index 0000000..66fb2e7 Binary files /dev/null and b/public/fonts/ClashDisplay-Regular.woff2 differ diff --git a/public/fonts/ClashDisplay-Semibold.woff2 b/public/fonts/ClashDisplay-Semibold.woff2 new file mode 100644 index 0000000..83ae6af Binary files /dev/null and b/public/fonts/ClashDisplay-Semibold.woff2 differ diff --git a/src/App.css b/src/App.css index 95f8aa6..aae77cd 100644 --- a/src/App.css +++ b/src/App.css @@ -1,39 +1,412 @@ -main { + +/* Main page */ +.catalogue-page { + min-height: 100vh; + background: #F4E3B2; + color: #310E10; +} + +/* Header */ +.site-header { + min-height: 88px; + padding: 20px 5%; display: flex; + align-items: center; + justify-content: space-between; + gap: 24px; + border-bottom: 1px solid #947268; +} + +.logo { + color: #74070E; + font-family: 'Clash Display', sans-serif; + font-size: clamp(1.8rem, 3vw, 2.5rem); + font-weight: 600; + letter-spacing: -1.5px; + text-decoration: none; +} + +.logo span, +.catalogue-heading h1 span { + color: #74070E; +} + +.header-actions { + display: flex; + align-items: center; + gap: 12px; +} + +.header-button { + padding: 10px 18px; + border: 1px solid #74070E; + border-radius: 3px; + background: transparent; + color: #74070E; + font-size: 0.9rem; + cursor: pointer; +} + +.sign-in-button { + background: #74070E; + color: #F4E3B2; +} + +.header-button:hover { + background: #310E10; + border-color: #310E10; + color: #F4E3B2; +} + +/* Main content */ +.catalogue-main { + max-width: 1600px; + margin: 0 auto; + padding: 44px 5% 60px; +} + +.catalogue-heading { + display: flex; + justify-content: space-between; + align-items: end; gap: 20px; - padding: 20px; + margin-bottom: 30px; } -.filter-box { - width: 220px; - border: 1px solid black; - padding: 10px; +.eyebrow { + margin: 0 0 8px; + color: #74070E; + font-size: 0.75rem; + font-weight: 700; + letter-spacing: 2px; +} + +.catalogue-heading h1 { + margin: 0; + font-family: 'Clash Display', sans-serif; + font-size: clamp(2rem, 4vw, 3.2rem); + line-height: 1.05; +} + +.toggle-filters { + padding: 10px 14px; + border: 1px solid #45462A; + border-radius: 3px; + background: transparent; + color: #310E10; + cursor: pointer; + white-space: nowrap; +} + +.toggle-filters:hover { + background: #45462A; + color: #F4E3B2; } -.filters { +/* Catalogue layout */ +.catalogue-layout { + display: grid; + grid-template-columns: 230px minmax(0, 1fr); + align-items: start; + gap: 28px; +} + +.catalogue-layout.filters-hidden { + grid-template-columns: minmax(0, 1fr); +} + +/* Filter panel */ +.filter-box { display: flex; flex-direction: column; gap: 10px; + padding: 20px; + border: 1px solid #947268; + border-radius: 3px; + background: #fff4d5; } -.album-displayer { +.filter-box h2 { + margin: 0 0 12px; + font-family: 'Clash Display', sans-serif; + font-size: 1.35rem; +} + +.filter-box label { + margin-top: 8px; + font-size: 0.85rem; + font-weight: 600; +} + +.filter-box input, +.filter-box select { + width: 100%; + min-height: 42px; + padding: 10px; + border: 1px solid #947268; + border-radius: 2px; + background: #fffaf0; + color: #310E10; + font: inherit; +} + +.filter-box input:focus, +.filter-box select:focus { + outline: 2px solid #74070E; + outline-offset: 2px; +} + +.rating { + margin-top: 14px; +} + +.rating h3 { + margin: 0; + font-family: 'Clash Display', sans-serif; + font-size: 1.1rem; +} + +.filter-hint { + margin: 6px 0 10px; + color: #45462A; + font-size: 0.75rem; + line-height: 1.5; +} + +.rating-buttons { + display: flex; + gap: 6px; +} + +.rating-buttons button { flex: 1; + min-width: 0; + padding: 9px 0; + border: 1px solid #45462A; + border-radius: 2px; + background: transparent; + color: #310E10; + cursor: pointer; +} + +.rating-buttons button:hover, +.rating-buttons button.selected { + background: #74070E; + border-color: #74070E; + color: #F4E3B2; +} + +.clear-filters { + margin-top: 12px; + padding: 10px; + border: 0; + background: #45462A; + color: #F4E3B2; + cursor: pointer; +} + +.clear-filters:hover { + background: #310E10; +} + +/* Album grid */ +.album-section { + min-width: 0; +} + +.results-heading { + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; + margin-bottom: 18px; +} + +.results-heading p { + margin: 0; + font-size: 0.9rem; +} + +.results-heading span { + color: #74070E; + font-size: 0.7rem; + font-weight: 700; + letter-spacing: 1.3px; + text-align: right; +} + +.album-displayer { display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 10px; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 24px 18px; +} + +.filters-hidden .album-displayer { + grid-template-columns: repeat(4, minmax(0, 1fr)); } .album-card { - border: 1px solid black; - padding: 10px; - min-height: 180px; + min-width: 0; + overflow: hidden; + color: #310E10; + text-decoration: none; + transition: transform 0.2s ease; +} + +.album-card:hover { + transform: translateY(-4px); } .album-image { - height: 120px; - border: 1px solid black; + display: block; + width: 100%; + aspect-ratio: 1 / 1; + object-fit: cover; + border: 1px solid #947268; + background: #947268; +} +.album-placeholder { + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: flex-start; + padding: 18px; + background: #74070E; + color: #F4E3B2; +} + +.album-card:nth-child(2n) .album-placeholder { + background: #45462A; +} + +.album-card:nth-child(3n) .album-placeholder { + background: #947268; + color: #310E10; +} + +.placeholder-mark { + font-family: 'Clash Display', sans-serif; + font-size: 2.8rem; + font-weight: 600; +} + +.placeholder-title { + max-width: 100%; + font-family: 'Clash Display', sans-serif; + font-size: clamp(1.1rem, 1.7vw, 1.8rem); + font-weight: 600; + overflow-wrap: anywhere; +} + +.album-card-info { + padding: 12px 2px 0; +} + +.album-card-info h2 { + margin: 0 0 5px; + font-family: 'Clash Display', sans-serif; + font-size: 1.15rem; + line-height: 1.2; +} + +.album-artist { + margin: 0 0 10px; + color: #45462A; + font-size: 0.9rem; +} + +.album-meta { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + gap: 6px; + color: #45462A; + font-size: 0.75rem; +} + +/* Empty state and pagination */ +.empty-state { + padding: 48px 20px; + border: 1px dashed #947268; + text-align: center; +} + +.empty-state h2 { + font-family: 'Clash Display', sans-serif; +} + +.pagination { display: flex; - align-items: center; justify-content: center; + align-items: center; + gap: 18px; + margin-top: 32px; +} + +.pagination button { + padding: 10px 14px; + border: 1px solid #74070E; + border-radius: 2px; + background: transparent; + color: #74070E; +} + +.pagination button:disabled { + opacity: 0.45; + cursor: not-allowed; +} + +.pagination span { + font-size: 0.85rem; +} + +/* Smaller screens */ +@media (max-width: 1050px) { + .catalogue-layout { + grid-template-columns: 200px minmax(0, 1fr); + gap: 20px; + } + + .album-displayer, + .filters-hidden .album-displayer { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } } + +@media (max-width: 650px) { + .site-header { + padding: 18px 5%; + } + + .header-actions { + gap: 6px; + } + + .header-button { + padding: 9px 10px; + } + + .catalogue-main { + padding-top: 30px; + } + + .catalogue-heading { + align-items: flex-start; + flex-direction: column; + } + + .catalogue-layout { + grid-template-columns: minmax(0, 1fr); + } + + .album-displayer, + .filters-hidden .album-displayer { + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 20px 12px; + } + + .results-heading span { + display: none; + } +} \ No newline at end of file diff --git a/src/index.css b/src/index.css index 8b13789..7361d4a 100644 --- a/src/index.css +++ b/src/index.css @@ -1 +1,58 @@ +@import url('https://fonts.googleapis.com/css2?family=Archivo:wght@400;500;600;700&display=swap'); + +@font-face { + font-family: 'Clash Display'; + src: url('/fonts/ClashDisplay-Regular.woff2') format('woff2'); + font-weight: 400; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'Clash Display'; + src: url('/fonts/ClashDisplay-Semibold.woff2') format('woff2'); + font-weight: 600; + font-style: normal; + font-display: swap; +} + +:root { + font-family: 'Archivo', sans-serif; + color: #310E10; + background: #F4E3B2; + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + min-width: 320px; + min-height: 100vh; +} + +button, +input, +select { + font: inherit; +} + +button { + cursor: pointer; +} + +button:focus-visible, +a:focus-visible { + outline: 2px solid #74070E; + outline-offset: 3px; +} + +a { + color: inherit; +} \ No newline at end of file diff --git a/src/mock/albums.json b/src/mock/albums.json index 84a41ce..4927b70 100644 --- a/src/mock/albums.json +++ b/src/mock/albums.json @@ -5,13 +5,10 @@ "artist": "Radiohead", "releaseYear": 2007, "genre": ["Rock"], - "coverUrl": "link", - "audioUrl": "link", - "isFavorite": false, + "coverUrl": "", "tracks": [ { "title": "15 Step", "duration": "3:57" }, - { "title": "Bodysnatcher", "duration": "4:01" }, - { "title": "Nude", "duration": "4:15" } + { "title": "Bodysnatchers", "duration": "4:02" } ] }, { @@ -20,26 +17,58 @@ "artist": "Michael Jackson", "releaseYear": 2002, "genre": ["Pop"], - "coverUrl": "link", - "audioUrl": "link", - "isFavorite": false, + "coverUrl": "", "tracks": [ - { "title": "Billie Jean", "duration": "4:53" }, - { "title": "Beat It", "duration": "4:19" } + { "title": "Billie Jean", "duration": "4:54" }, + { "title": "Beat It", "duration": "4:18" } ] }, { "id": "album-3", - "title": "9 To 5 And Odd Jobs", + "title": "9 to 5 and Odd Jobs", "artist": "Dolly Parton", "releaseYear": 1980, "genre": ["Country"], - "coverUrl": "link", - "audioUrl": "link", - "isFavorite": false, + "coverUrl": "", "tracks": [ - { "title": "9 to 5", "duration": "2:46" }, - { "title": "Hush-A-Bye Hard Times", "duration": "3:50" } + { "title": "9 to 5", "duration": "2:43" }, + { "title": "Hush-A-Bye Hard Times", "duration": "3:30" } + ] + }, + { + "id": "album-4", + "title": "Rumours", + "artist": "Fleetwood Mac", + "releaseYear": 1977, + "genre": ["Rock"], + "coverUrl": "", + "tracks": [ + { "title": "Dreams", "duration": "4:17" }, + { "title": "The Chain", "duration": "4:30" } + ] + }, + { + "id": "album-5", + "title": "Back to Black", + "artist": "Amy Winehouse", + "releaseYear": 2006, + "genre": ["Soul", "R&B"], + "coverUrl": "", + "tracks": [ + { "title": "Rehab", "duration": "3:34" }, + { "title": "Back to Black", "duration": "4:01" } + ] + }, + { + "id": "album-6", + "title": "Discovery", + "artist": "Daft Punk", + "releaseYear": 2001, + "genre": ["Electronic"], + "coverUrl": "", + "tracks": [ + { "title": "One More Time", "duration": "5:20" }, + { "title": "Digital Love", "duration": "4:58" } ] } ] \ No newline at end of file diff --git a/src/pages/CataloguePage.tsx b/src/pages/CataloguePage.tsx index 00c1597..74c90b9 100644 --- a/src/pages/CataloguePage.tsx +++ b/src/pages/CataloguePage.tsx @@ -1,79 +1,252 @@ + +import { useMemo, useState } from 'react' +import { Link } from 'react-router-dom' import '../App.css' import albumData from '../mock/albums.json' import type { Album } from '../types/album' function CataloguePage() { -const albums: Album[] = albumData as Album[] + const albums: Album[] = albumData as Album[] + + const [search, setSearch] = useState('') + const [genre, setGenre] = useState('All genres') + const [decade, setDecade] = useState('All decades') + const [rating, setRating] = useState(null) + const [filtersVisible, setFiltersVisible] = useState(true) + + +const genres = [ + 'All genres', + 'Rock', + 'Jazz', + 'Pop', + 'Country', + 'HipHop', + 'Tekno', + 'Indie', + 'RnB', + 'Soul', + 'Kpop', + 'Funk', + 'Blues', + 'Classic', + 'Metal', +] + +const decades = [ + 'All decades', + '1920s', + '1930s', + '1940s', + '1950s', + '1960s', + '1970s', + '1980s', + '1990s', + '2000s', + '2010s', + '2020s', +] + + const filteredAlbums = useMemo(() => { + return albums.filter((album) => { + const matchesSearch = + `${album.title} ${album.artist}` + .toLowerCase() + .includes(search.toLowerCase()) + + const matchesGenre = + genre === 'All genres' || album.genre.includes(genre) + + const albumDecade = `${Math.floor(album.releaseYear / 10) * 10}s` + const matchesDecade = + decade === 'All decades' || albumDecade === decade + + // Rating er foreløpig bare en UI-kontroll, siden mock-dataene + // ikke inneholder faktiske ratingverdier. + const matchesRating = rating === null + + return matchesSearch && matchesGenre && matchesDecade && matchesRating + }) + }, [albums, search, genre, decade, rating]) + + const visibleAlbums = filteredAlbums.slice(0, filtersVisible ? 6 : 8) return ( -
-
- - - -
- - - - - - -
-

Rating

- - - - - +
+
+ + CDLLECTION. + + + +
+ +
+
+
+

YOUR MUSIC, YOUR COLLECTION

+

Explore albums.

+ +
-
-
- {albums.map((album) => ( -
-
Image
+
+ {filtersVisible && ( +
- ))} -
-
+ + setSearch(event.target.value)} + /> + + + + + + + +
+

Rating

+

Choose a rating

+
+ {[1, 2, 3, 4, 5].map((number) => ( + + ))} +
+

+ Rating filtering will work when rating data is available. +

+
+ + + + )} + +
+
+

+ {filteredAlbums.length}{' '} + {filteredAlbums.length === 1 ? 'album' : 'albums'} found +

+ CURATED FOR YOU +
+ + {visibleAlbums.length > 0 ? ( +
+ {visibleAlbums.map((album) => ( + + {album.coverUrl ? ( + {`${album.title} + ) : ( +
+ C. + + {album.title} + +
+ )} + +
+

{album.title}

+

{album.artist}

+
+ {album.releaseYear} + {album.genre.join(' / ')} +
+
+ + ))} +
+ ) : ( +
+

No albums found

+

Try another search or clear your filters.

+
+ )} + +
+ + Page 1 + +
+
+ + + ) }