added playwright, adjust location card, added fallback if image not found or 4xx

This commit is contained in:
goro 2026-03-16 10:43:40 +02:00
parent 80fa81ae4b
commit c51f187793
8 changed files with 319 additions and 88 deletions

27
.github/workflows/playwright.yml vendored Normal file
View File

@ -0,0 +1,27 @@
name: Playwright Tests
on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: lts/*
- name: Install dependencies
run: npm install -g pnpm && pnpm install
- name: Install Playwright Browsers
run: pnpm exec playwright install --with-deps
- name: Run Playwright tests
run: pnpm exec playwright test
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report
path: playwright-report/
retention-days: 30

9
.gitignore vendored
View File

@ -7,6 +7,8 @@ yarn-error.log*
pnpm-debug.log* pnpm-debug.log*
lerna-debug.log* lerna-debug.log*
.env
node_modules node_modules
dist dist
dist-ssr dist-ssr
@ -22,3 +24,10 @@ dist-ssr
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?
# Playwright
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
/playwright/.auth/

View File

@ -11,12 +11,15 @@
"dependencies": { "dependencies": {
"@floating-ui/react": "^0.26.9", "@floating-ui/react": "^0.26.9",
"@reduxjs/toolkit": "^1.9.5", "@reduxjs/toolkit": "^1.9.5",
"@tanstack/react-query": "^5.90.21",
"axios": "^1.5.0", "axios": "^1.5.0",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"emojibase": "^15.0.0", "emojibase": "^15.0.0",
"interweave": "^13.1.0", "interweave": "^13.1.0",
"interweave-autolink": "^5.1.0", "interweave-autolink": "^5.1.0",
"interweave-emoji": "^7.0.0", "interweave-emoji": "^7.0.0",
"lucide-react": "^0.510.0",
"preact": "^10.16.0", "preact": "^10.16.0",
"react-redux": "^8.1.2", "react-redux": "^8.1.2",
"react-router-dom": "^6.16.0", "react-router-dom": "^6.16.0",
@ -24,10 +27,13 @@
"react-textarea-autosize": "^8.5.3", "react-textarea-autosize": "^8.5.3",
"redux-persist": "^6.0.0", "redux-persist": "^6.0.0",
"redux-thunk": "^2.4.2", "redux-thunk": "^2.4.2",
"tailwind-merge": "^3.5.0",
"yet-another-react-lightbox": "^3.12.2" "yet-another-react-lightbox": "^3.12.2"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.58.2",
"@preact/preset-vite": "^2.5.0", "@preact/preset-vite": "^2.5.0",
"@types/node": "^25.5.0",
"@types/react-redux": "^7.1.34", "@types/react-redux": "^7.1.34",
"autoprefixer": "^10.4.15", "autoprefixer": "^10.4.15",
"postcss": "^8.4.28", "postcss": "^8.4.28",

80
playwright.config.ts Normal file
View File

@ -0,0 +1,80 @@
/// <reference types="node" />
import { defineConfig, devices } from '@playwright/test';
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// import dotenv from 'dotenv';
// import path from 'path';
// dotenv.config({ path: path.resolve(__dirname, '.env') });
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './tests',
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('')`. */
// baseURL: 'http://localhost:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
},
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
],
/* Run your local dev server before starting the tests */
// webServer: {
// command: 'npm run start',
// url: 'http://localhost:3000',
// reuseExistingServer: !process.env.CI,
// },
});

135
pnpm-lock.yaml generated
View File

@ -14,12 +14,18 @@ importers:
'@reduxjs/toolkit': '@reduxjs/toolkit':
specifier: ^1.9.5 specifier: ^1.9.5
version: 1.9.7(react-redux@8.1.3(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(redux@4.2.1))(react@18.2.0) version: 1.9.7(react-redux@8.1.3(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(redux@4.2.1))(react@18.2.0)
'@tanstack/react-query':
specifier: ^5.90.21
version: 5.90.21(react@18.2.0)
axios: axios:
specifier: ^1.5.0 specifier: ^1.5.0
version: 1.6.7 version: 1.6.7
class-variance-authority: class-variance-authority:
specifier: ^0.7.0 specifier: ^0.7.0
version: 0.7.0 version: 0.7.0
clsx:
specifier: ^2.1.1
version: 2.1.1
emojibase: emojibase:
specifier: ^15.0.0 specifier: ^15.0.0
version: 15.3.0 version: 15.3.0
@ -32,6 +38,9 @@ importers:
interweave-emoji: interweave-emoji:
specifier: ^7.0.0 specifier: ^7.0.0
version: 7.0.0(interweave@13.1.0(react@18.2.0))(react@18.2.0) version: 7.0.0(interweave@13.1.0(react@18.2.0))(react@18.2.0)
lucide-react:
specifier: ^0.510.0
version: 0.510.0(react@18.2.0)
preact: preact:
specifier: ^10.16.0 specifier: ^10.16.0
version: 10.19.3 version: 10.19.3
@ -53,13 +62,22 @@ importers:
redux-thunk: redux-thunk:
specifier: ^2.4.2 specifier: ^2.4.2
version: 2.4.2(redux@4.2.1) version: 2.4.2(redux@4.2.1)
tailwind-merge:
specifier: ^3.5.0
version: 3.5.0
yet-another-react-lightbox: yet-another-react-lightbox:
specifier: ^3.12.2 specifier: ^3.12.2
version: 3.16.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) version: 3.16.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
devDependencies: devDependencies:
'@playwright/test':
specifier: ^1.58.2
version: 1.58.2
'@preact/preset-vite': '@preact/preset-vite':
specifier: ^2.5.0 specifier: ^2.5.0
version: 2.8.1(@babel/core@7.23.9)(preact@10.19.3)(vite@4.5.2) version: 2.8.1(@babel/core@7.23.9)(preact@10.19.3)(vite@4.5.2(@types/node@25.5.0))
'@types/node':
specifier: ^25.5.0
version: 25.5.0
'@types/react-redux': '@types/react-redux':
specifier: ^7.1.34 specifier: ^7.1.34
version: 7.1.34 version: 7.1.34
@ -77,7 +95,7 @@ importers:
version: 5.3.3 version: 5.3.3
vite: vite:
specifier: ^4.4.5 specifier: ^4.4.5
version: 4.5.2 version: 4.5.2(@types/node@25.5.0)
packages: packages:
@ -441,6 +459,11 @@ packages:
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'} engines: {node: '>=14'}
'@playwright/test@1.58.2':
resolution: {integrity: sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==}
engines: {node: '>=18'}
hasBin: true
'@preact/preset-vite@2.8.1': '@preact/preset-vite@2.8.1':
resolution: {integrity: sha512-a9KV4opdj17X2gOFuGup0aE+sXYABX/tJi/QDptOrleX4FlnoZgDWvz45tHOdVfrZX+3uvVsIYPHxRsTerkDNA==} resolution: {integrity: sha512-a9KV4opdj17X2gOFuGup0aE+sXYABX/tJi/QDptOrleX4FlnoZgDWvz45tHOdVfrZX+3uvVsIYPHxRsTerkDNA==}
peerDependencies: peerDependencies:
@ -483,9 +506,20 @@ packages:
resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==} resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==}
engines: {node: '>= 8.0.0'} engines: {node: '>= 8.0.0'}
'@tanstack/query-core@5.90.20':
resolution: {integrity: sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==}
'@tanstack/react-query@5.90.21':
resolution: {integrity: sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg==}
peerDependencies:
react: ^18 || ^19
'@types/hoist-non-react-statics@3.3.5': '@types/hoist-non-react-statics@3.3.5':
resolution: {integrity: sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==} resolution: {integrity: sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==}
'@types/node@25.5.0':
resolution: {integrity: sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==}
'@types/parse-json@4.0.2': '@types/parse-json@4.0.2':
resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==}
@ -589,8 +623,8 @@ packages:
resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
caniuse-lite@1.0.30001580: caniuse-lite@1.0.30001754:
resolution: {integrity: sha512-mtj5ur2FFPZcCEpXFy8ADXbDACuNFXg6mxVDqp7tqooX6l3zwm+d8EPoeOSIFRDvHs8qu7/SLFOGniULkcH2iA==} resolution: {integrity: sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg==}
chalk@2.4.2: chalk@2.4.2:
resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==}
@ -607,6 +641,10 @@ packages:
resolution: {integrity: sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==} resolution: {integrity: sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==}
engines: {node: '>=6'} engines: {node: '>=6'}
clsx@2.1.1:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'}
color-convert@1.9.3: color-convert@1.9.3:
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
@ -777,6 +815,11 @@ packages:
fraction.js@4.3.7: fraction.js@4.3.7:
resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
fsevents@2.3.2:
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
fsevents@2.3.3: fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@ -923,6 +966,11 @@ packages:
lru-cache@5.1.1: lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
lucide-react@0.510.0:
resolution: {integrity: sha512-p8SQRAMVh7NhsAIETokSqDrc5CHnDLbV29mMnzaXx+Vc/hnqQzwI2r0FMWCcoTXnbw2KEjy48xwpGdEL+ck06Q==}
peerDependencies:
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
magic-string@0.30.5: magic-string@0.30.5:
resolution: {integrity: sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==} resolution: {integrity: sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==}
engines: {node: '>=12'} engines: {node: '>=12'}
@ -1028,6 +1076,16 @@ packages:
resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
playwright-core@1.58.2:
resolution: {integrity: sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==}
engines: {node: '>=18'}
hasBin: true
playwright@1.58.2:
resolution: {integrity: sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==}
engines: {node: '>=18'}
hasBin: true
postcss-import@15.1.0: postcss-import@15.1.0:
resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==}
engines: {node: '>=14.0.0'} engines: {node: '>=14.0.0'}
@ -1198,8 +1256,8 @@ packages:
run-parallel@1.2.0: run-parallel@1.2.0:
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
scheduler@0.23.0: scheduler@0.23.2:
resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==}
semver@6.3.1: semver@6.3.1:
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
@ -1260,6 +1318,9 @@ packages:
tabbable@6.2.0: tabbable@6.2.0:
resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==} resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==}
tailwind-merge@3.5.0:
resolution: {integrity: sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==}
tailwindcss@3.4.1: tailwindcss@3.4.1:
resolution: {integrity: sha512-qAYmXRfk3ENzuPBakNK0SRrUDipP8NQnEY6772uDhflcQz5EhRdD7JNZxyrFHVQNCwULPBn6FNPp9brpO7ctcA==} resolution: {integrity: sha512-qAYmXRfk3ENzuPBakNK0SRrUDipP8NQnEY6772uDhflcQz5EhRdD7JNZxyrFHVQNCwULPBn6FNPp9brpO7ctcA==}
engines: {node: '>=14.0.0'} engines: {node: '>=14.0.0'}
@ -1288,6 +1349,9 @@ packages:
engines: {node: '>=14.17'} engines: {node: '>=14.17'}
hasBin: true hasBin: true
undici-types@7.18.2:
resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==}
update-browserslist-db@1.0.13: update-browserslist-db@1.0.13:
resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==} resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==}
hasBin: true hasBin: true
@ -1744,12 +1808,16 @@ snapshots:
'@pkgjs/parseargs@0.11.0': '@pkgjs/parseargs@0.11.0':
optional: true optional: true
'@preact/preset-vite@2.8.1(@babel/core@7.23.9)(preact@10.19.3)(vite@4.5.2)': '@playwright/test@1.58.2':
dependencies:
playwright: 1.58.2
'@preact/preset-vite@2.8.1(@babel/core@7.23.9)(preact@10.19.3)(vite@4.5.2(@types/node@25.5.0))':
dependencies: dependencies:
'@babel/core': 7.23.9 '@babel/core': 7.23.9
'@babel/plugin-transform-react-jsx': 7.23.4(@babel/core@7.23.9) '@babel/plugin-transform-react-jsx': 7.23.4(@babel/core@7.23.9)
'@babel/plugin-transform-react-jsx-development': 7.22.5(@babel/core@7.23.9) '@babel/plugin-transform-react-jsx-development': 7.22.5(@babel/core@7.23.9)
'@prefresh/vite': 2.4.5(preact@10.19.3)(vite@4.5.2) '@prefresh/vite': 2.4.5(preact@10.19.3)(vite@4.5.2(@types/node@25.5.0))
'@rollup/pluginutils': 4.2.1 '@rollup/pluginutils': 4.2.1
babel-plugin-transform-hook-names: 1.0.2(@babel/core@7.23.9) babel-plugin-transform-hook-names: 1.0.2(@babel/core@7.23.9)
debug: 4.3.4 debug: 4.3.4
@ -1757,7 +1825,7 @@ snapshots:
magic-string: 0.30.5 magic-string: 0.30.5
node-html-parser: 6.1.12 node-html-parser: 6.1.12
resolve: 1.22.8 resolve: 1.22.8
vite: 4.5.2 vite: 4.5.2(@types/node@25.5.0)
transitivePeerDependencies: transitivePeerDependencies:
- preact - preact
- supports-color - supports-color
@ -1770,7 +1838,7 @@ snapshots:
'@prefresh/utils@1.2.0': {} '@prefresh/utils@1.2.0': {}
'@prefresh/vite@2.4.5(preact@10.19.3)(vite@4.5.2)': '@prefresh/vite@2.4.5(preact@10.19.3)(vite@4.5.2(@types/node@25.5.0))':
dependencies: dependencies:
'@babel/core': 7.23.9 '@babel/core': 7.23.9
'@prefresh/babel-plugin': 0.5.1 '@prefresh/babel-plugin': 0.5.1
@ -1778,7 +1846,7 @@ snapshots:
'@prefresh/utils': 1.2.0 '@prefresh/utils': 1.2.0
'@rollup/pluginutils': 4.2.1 '@rollup/pluginutils': 4.2.1
preact: 10.19.3 preact: 10.19.3
vite: 4.5.2 vite: 4.5.2(@types/node@25.5.0)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@ -1799,11 +1867,22 @@ snapshots:
estree-walker: 2.0.2 estree-walker: 2.0.2
picomatch: 2.3.1 picomatch: 2.3.1
'@tanstack/query-core@5.90.20': {}
'@tanstack/react-query@5.90.21(react@18.2.0)':
dependencies:
'@tanstack/query-core': 5.90.20
react: 18.2.0
'@types/hoist-non-react-statics@3.3.5': '@types/hoist-non-react-statics@3.3.5':
dependencies: dependencies:
'@types/react': 18.2.48 '@types/react': 18.2.48
hoist-non-react-statics: 3.3.2 hoist-non-react-statics: 3.3.2
'@types/node@25.5.0':
dependencies:
undici-types: 7.18.2
'@types/parse-json@4.0.2': {} '@types/parse-json@4.0.2': {}
'@types/prop-types@15.7.11': {} '@types/prop-types@15.7.11': {}
@ -1857,7 +1936,7 @@ snapshots:
autoprefixer@10.4.17(postcss@8.4.33): autoprefixer@10.4.17(postcss@8.4.33):
dependencies: dependencies:
browserslist: 4.22.2 browserslist: 4.22.2
caniuse-lite: 1.0.30001580 caniuse-lite: 1.0.30001754
fraction.js: 4.3.7 fraction.js: 4.3.7
normalize-range: 0.1.2 normalize-range: 0.1.2
picocolors: 1.0.0 picocolors: 1.0.0
@ -1898,7 +1977,7 @@ snapshots:
browserslist@4.22.2: browserslist@4.22.2:
dependencies: dependencies:
caniuse-lite: 1.0.30001580 caniuse-lite: 1.0.30001754
electron-to-chromium: 1.4.646 electron-to-chromium: 1.4.646
node-releases: 2.0.14 node-releases: 2.0.14
update-browserslist-db: 1.0.13(browserslist@4.22.2) update-browserslist-db: 1.0.13(browserslist@4.22.2)
@ -1907,7 +1986,7 @@ snapshots:
camelcase-css@2.0.1: {} camelcase-css@2.0.1: {}
caniuse-lite@1.0.30001580: {} caniuse-lite@1.0.30001754: {}
chalk@2.4.2: chalk@2.4.2:
dependencies: dependencies:
@ -1933,6 +2012,8 @@ snapshots:
clsx@2.0.0: {} clsx@2.0.0: {}
clsx@2.1.1: {}
color-convert@1.9.3: color-convert@1.9.3:
dependencies: dependencies:
color-name: 1.1.3 color-name: 1.1.3
@ -2104,6 +2185,9 @@ snapshots:
fraction.js@4.3.7: {} fraction.js@4.3.7: {}
fsevents@2.3.2:
optional: true
fsevents@2.3.3: fsevents@2.3.3:
optional: true optional: true
@ -2221,6 +2305,10 @@ snapshots:
dependencies: dependencies:
yallist: 3.1.1 yallist: 3.1.1
lucide-react@0.510.0(react@18.2.0):
dependencies:
react: 18.2.0
magic-string@0.30.5: magic-string@0.30.5:
dependencies: dependencies:
'@jridgewell/sourcemap-codec': 1.4.15 '@jridgewell/sourcemap-codec': 1.4.15
@ -2305,6 +2393,14 @@ snapshots:
pirates@4.0.6: {} pirates@4.0.6: {}
playwright-core@1.58.2: {}
playwright@1.58.2:
dependencies:
playwright-core: 1.58.2
optionalDependencies:
fsevents: 2.3.2
postcss-import@15.1.0(postcss@8.4.33): postcss-import@15.1.0(postcss@8.4.33):
dependencies: dependencies:
postcss: 8.4.33 postcss: 8.4.33
@ -2358,7 +2454,7 @@ snapshots:
dependencies: dependencies:
loose-envify: 1.4.0 loose-envify: 1.4.0
react: 18.2.0 react: 18.2.0
scheduler: 0.23.0 scheduler: 0.23.2
react-is@16.13.1: {} react-is@16.13.1: {}
@ -2472,7 +2568,7 @@ snapshots:
dependencies: dependencies:
queue-microtask: 1.2.3 queue-microtask: 1.2.3
scheduler@0.23.0: scheduler@0.23.2:
dependencies: dependencies:
loose-envify: 1.4.0 loose-envify: 1.4.0
@ -2530,6 +2626,8 @@ snapshots:
tabbable@6.2.0: {} tabbable@6.2.0: {}
tailwind-merge@3.5.0: {}
tailwindcss@3.4.1: tailwindcss@3.4.1:
dependencies: dependencies:
'@alloc/quick-lru': 5.2.0 '@alloc/quick-lru': 5.2.0
@ -2575,6 +2673,8 @@ snapshots:
typescript@5.3.3: {} typescript@5.3.3: {}
undici-types@7.18.2: {}
update-browserslist-db@1.0.13(browserslist@4.22.2): update-browserslist-db@1.0.13(browserslist@4.22.2):
dependencies: dependencies:
browserslist: 4.22.2 browserslist: 4.22.2
@ -2604,12 +2704,13 @@ snapshots:
util-deprecate@1.0.2: {} util-deprecate@1.0.2: {}
vite@4.5.2: vite@4.5.2(@types/node@25.5.0):
dependencies: dependencies:
esbuild: 0.18.20 esbuild: 0.18.20
postcss: 8.4.33 postcss: 8.4.33
rollup: 3.29.4 rollup: 3.29.4
optionalDependencies: optionalDependencies:
'@types/node': 25.5.0
fsevents: 2.3.3 fsevents: 2.3.3
which@2.0.2: which@2.0.2:

View File

@ -1,5 +1,6 @@
import { JSXInternal } from "node_modules/preact/src/jsx"; import { JSXInternal } from "node_modules/preact/src/jsx";
import { LocationInfo } from "../../../domains"; import { LocationInfo } from "../../../domains";
import { cn } from "../../../utils/common";
interface ComponentProps { interface ComponentProps {
onCardClick: (id: number) => void, onCardClick: (id: number) => void,
@ -8,40 +9,70 @@ interface ComponentProps {
containerStyle?: JSXInternal.CSSProperties containerStyle?: JSXInternal.CSSProperties
} }
const LocationCard = (props: ComponentProps) => ( const LocationCard = (props: ComponentProps) => {
const getScoreClasses = (score: number, count: number) => {
if (count === 0) return {
text: 'text-white',
bg: 'bg-white'
};
if (score <= 40) return {
text: 'text-rating-red',
bg: 'bg-rating-red'
};
if (score <= 69) return {
text: 'text-rating-yellow',
bg: 'bg-rating-yellow'
};
return {
text: 'text-brand-green',
bg: 'bg-brand-green'
};
};
const criticClasses = getScoreClasses(props.data.critic_score, props.data.critic_count);
const userClasses = getScoreClasses(props.data.user_score, props.data.user_count);
return (
<div className={props.containerClass} style={props.containerStyle}> <div className={props.containerClass} style={props.containerStyle}>
<a onClick={() => props.onCardClick(props.data.id)}> <a onClick={() => props.onCardClick(props.data.id)}>
<div className={'border-secondary recently-img-container'}> <div className={'border-secondary recently-img-container'}>
<img alt={props.data.name} src={props.data.thumbnail ? props.data.thumbnail : ''} loading="lazy" style={{ width: '100%', height: '100%' }} /> <img
alt={props.data.name}
src={props.data.thumbnail ? props.data.thumbnail : ''}
loading="lazy"
style={{ width: '100%', height: '100%' }}
onError={(e) => {
e.currentTarget.src = 'https://pub-6b637ea51b64436dbf0514bc956972d1.r2.dev/public/upload/misty-forest-black-white.webp';
e.currentTarget.onerror = null;
}}
/>
</div> </div>
</a> </a>
<div className={"border-primary pb-2 location-container text-sm mb-2 mt-2"}> <div className={"border-primary pb-2 location-container text-sm mb-2 mt-2"}>
<p className={'location-title'}>{props.data.name}</p> <p className={'location-title'}>{props.data.name}</p>
<p className={'text-xs mt-1'}>{props.data.regency_name}, {props.data.province_name}</p> <p className={'text-xs mt-1 h-8 line-clamp-2'}>{props.data.regency_name}, {props.data.province_name}</p>
</div> </div>
{props.data.critic_count !== 0 &&
<div className={"flex flex-row items-center mb-3"}> <div className={"flex flex-row items-center mb-3"}>
<div className={'mr-3 users-score-bar'}> <div className={'mr-3 users-score-bar'}>
<p className={'text-sm text-center'}>{props.data.critic_score}</p> <p className={cn('text-md text-center', criticClasses.text)}>{props.data.critic_count !== 0 ? props.data.critic_score : 'NR'}</p>
<div style={{ height: 4, width: 30, backgroundColor: "#72767d" }}> <div className="h-1 w-[30px] bg-[#72767d] rounded-full overflow-hidden">
<div style={{ height: 4, width: `${props.data.critic_score}%`, backgroundColor: 'green' }} /> <div className={cn('h-full rounded-full', criticClasses.bg)} style={{ width: `${props.data.critic_score}%` }} />
</div> </div>
</div> </div>
<p className={"users-score"}>critic score ({props.data.critic_count})</p> <p className={"users-score"}>Critic score ({props.data.critic_count})</p>
</div> </div>
}
{props.data.user_score !== 0 &&
<div className={"flex flex-row items-center"}> <div className={"flex flex-row items-center"}>
<div className={'mr-3 users-score-bar'}> <div className={'mr-3 users-score-bar'}>
<p className={'text-sm text-center'}>{props.data.user_score}</p> <p className={cn('text-md text-center', userClasses.text)}>{props.data.user_count !== 0 ? props.data.user_score : 'NR'}</p>
<div style={{ height: 4, width: 30, backgroundColor: "#72767d" }}> <div className="h-1 w-[30px] bg-[#72767d] rounded-full overflow-hidden">
<div style={{ height: 4, width: ` ${props.data.user_score}%`, backgroundColor: 'green' }} /> <div className={cn('h-full rounded-full', userClasses.bg)} style={{ width: `${props.data.user_score}%` }} />
</div> </div>
</div> </div>
<p className={'users-score'}>user score ({props.data.user_count})</p> <p className={'users-score'}>User score ({props.data.user_count})</p>
</div> </div>
</div>
);
} }
</div>
)
export default LocationCard; export default LocationCard;

View File

@ -2,17 +2,18 @@
export default { export default {
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
theme: { theme: {
extend: {
colors: { colors: {
transparent: 'transparent',
current: 'currentColor',
primary: '#202225', primary: '#202225',
secondary: '#2f3136', secondary: '#2f3136',
tertiary: '#a8adb3', tertiary: '#a8adb3',
quartenary: '#4D4E51', quartenary: '#4D4E51',
green: '#85CE73', 'brand-green': '#85CE73',
error: '#ff5454', error: '#ff5454',
gray: '#797979', 'brand-yellow': '#e5c453',
yellow: '#e5c453' 'rating-red': '#CE7385',
'rating-green': '#85CE73',
'rating-yellow': '#DECA21'
}, },
borderColor: { borderColor: {
primary: '#38444d', primary: '#38444d',
@ -23,32 +24,8 @@ export default {
}, },
fontSize: { fontSize: {
xxs: ['0.65rem', { lineHeight: '.85rem' }], xxs: ['0.65rem', { lineHeight: '.85rem' }],
xs: ['0.75rem', { lineHeight: '1rem' }],
sm: ['0.875rem', { lineHeight: '1.25rem' }],
base: ['1rem', { lineHeight: '1.5rem' }],
lg: ['1.125rem', { lineHeight: '1.75rem' }],
xl: ['1.25rem', { lineHeight: '1.75rem' }],
'2xl': ['1.5rem', { lineHeight: '2rem' }],
'3xl': ['1.875rem', { lineHeight: '2.25rem' }],
'4xl': ['2.25rem', { lineHeight: '2.5rem' }],
'5xl': ['3rem', { lineHeight: '1' }],
'6xl': ['3.75rem', { lineHeight: '1' }],
'7xl': ['4.5rem', { lineHeight: '1' }],
'8xl': ['6rem', { lineHeight: '1' }],
'9xl': ['8rem', { lineHeight: '1' }],
}, },
fontWeight: {
thin: '100',
extralight: '200',
light: '300',
normal: '400',
medium: '500',
semibold: '600',
bold: '700',
extrabold: '800',
black: '900',
}, },
extend: {},
}, },
plugins: [], plugins: [],
} }

View File

@ -10,7 +10,7 @@
"react": ["./node_modules/preact/compat/"], "react": ["./node_modules/preact/compat/"],
"react-dom": ["./node_modules/preact/compat/"] "react-dom": ["./node_modules/preact/compat/"]
}, },
"types": ["node"],
/* Bundler mode */ /* Bundler mode */
"moduleResolution": "bundler", "moduleResolution": "bundler",
"allowImportingTsExtensions": true, "allowImportingTsExtensions": true,