diff --git a/.eslintrc.json b/.eslintrc.json index 4d765f28..4eb6eb2c 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,3 +1,10 @@ { - "extends": ["next/core-web-vitals", "prettier"] + "extends": [ + "next/core-web-vitals", + "eslint:recommended", + "plugin:@typescript-eslint/recommended" + ], + "parser": "@typescript-eslint/parser", + "plugins": ["@typescript-eslint"], + "root": true } diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a75a8fee..82d90742 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,7 +16,9 @@ jobs: - name: Install dependencies run: npm ci - name: Run lint - run: npm run lint + run: | + npm run typecheck + npm run lint test: timeout-minutes: 10 runs-on: ubuntu-latest diff --git a/dev-server.mjs b/dev-server.mjs index a9809df5..bfe8c2a2 100644 --- a/dev-server.mjs +++ b/dev-server.mjs @@ -9,10 +9,11 @@ const basePath = nextConfig.basePath; const port = parseInt(process.env.PORT, 10) || 3404; const virkailijaOrigin = process.env.VIRKAILIJA_URL; +const isProd = process.env.NODE_ENV === 'production'; const app = next({ conf: nextConfig, - dev: true, + dev: !isProd, hostname: 'localhost', port: port, env: process.env, diff --git a/package-lock.json b/package-lock.json index 03f940b8..27e9ac26 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,23 +12,25 @@ "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.0", "@mui/icons-material": "^5.15.15", - "@mui/material": "^5.15.14", + "@mui/material": "^5.15.15", "@mui/material-nextjs": "^5.15.11", "@tanstack/react-query": "^5.29.2", "@tanstack/react-query-devtools": "^5.29.2", "cookie": "^0.6.0", - "next": "^14.2.0", + "next": "^14.2.2", "react": "^18", "react-dom": "^18" }, "devDependencies": { + "@axe-core/playwright": "^4.9.0", "@playwright/test": "^1.42.0", "@testing-library/react": "^14.2.2", "@types/cookie": "^0.6.0", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", - "@types/xml2js": "^0.4.14", + "@typescript-eslint/eslint-plugin": "^7.7.0", + "@typescript-eslint/parser": "^7.7.0", "@vitejs/plugin-react": "^4.2.1", "autoprefixer": "^10.0.1", "eslint": "^8", @@ -67,6 +69,27 @@ "node": ">=6.0.0" } }, + "node_modules/@axe-core/playwright": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.9.0.tgz", + "integrity": "sha512-Q1Lz75dNsX38jof+aev7RficDMdH/HLOLySkDdXR0fUoeFcLdw4UNgDO2CNNP4CTpoesEdfYRdd6VmDXjhBgbA==", + "dev": true, + "dependencies": { + "axe-core": "~4.9.0" + }, + "peerDependencies": { + "playwright-core": ">= 1.0.0" + } + }, + "node_modules/@axe-core/playwright/node_modules/axe-core": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.9.0.tgz", + "integrity": "sha512-H5orY+M2Fr56DWmMFpMrq5Ge93qjNdPVqzBv5gWK3aD1OvjBEJlEzxf09z93dGVQeI0LiW+aCMIx1QtShC/zUw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/@babel/code-frame": { "version": "7.24.2", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz", @@ -1333,9 +1356,9 @@ } }, "node_modules/@mui/core-downloads-tracker": { - "version": "5.15.14", - "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.15.14.tgz", - "integrity": "sha512-on75VMd0XqZfaQW+9pGjSNiqW+ghc5E2ZSLRBXwcXl/C4YzjfyjrLPhrEpKnR9Uym9KXBvxrhoHfPcczYHweyA==", + "version": "5.15.15", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.15.15.tgz", + "integrity": "sha512-aXnw29OWQ6I5A47iuWEI6qSSUfH6G/aCsW9KmW3LiFqr7uXZBK4Ks+z8G+qeIub8k0T5CMqlT2q0L+ZJTMrqpg==", "funding": { "type": "opencollective", "url": "https://opencollective.com/mui-org" @@ -1367,14 +1390,14 @@ } }, "node_modules/@mui/material": { - "version": "5.15.14", - "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.15.14.tgz", - "integrity": "sha512-kEbRw6fASdQ1SQ7LVdWR5OlWV3y7Y54ZxkLzd6LV5tmz+NpO3MJKZXSfgR0LHMP7meKsPiMm4AuzV0pXDpk/BQ==", + "version": "5.15.15", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.15.15.tgz", + "integrity": "sha512-3zvWayJ+E1kzoIsvwyEvkTUKVKt1AjchFFns+JtluHCuvxgKcLSRJTADw37k0doaRtVAsyh8bz9Afqzv+KYrIA==", "dependencies": { "@babel/runtime": "^7.23.9", "@mui/base": "5.0.0-beta.40", - "@mui/core-downloads-tracker": "^5.15.14", - "@mui/system": "^5.15.14", + "@mui/core-downloads-tracker": "^5.15.15", + "@mui/system": "^5.15.15", "@mui/types": "^7.2.14", "@mui/utils": "^5.15.14", "@types/react-transition-group": "^4.4.10", @@ -1507,9 +1530,9 @@ } }, "node_modules/@mui/system": { - "version": "5.15.14", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.15.14.tgz", - "integrity": "sha512-auXLXzUaCSSOLqJXmsAaq7P96VPRXg2Rrz6OHNV7lr+kB8lobUF+/N84Vd9C4G/wvCXYPs5TYuuGBRhcGbiBGg==", + "version": "5.15.15", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.15.15.tgz", + "integrity": "sha512-aulox6N1dnu5PABsfxVGOZffDVmlxPOVgj56HrUnJE8MCSh8lOvvkd47cebIVQQYAjpwieXQXiDPj5pwM40jTQ==", "dependencies": { "@babel/runtime": "^7.23.9", "@mui/private-theming": "^5.15.14", @@ -1591,9 +1614,9 @@ "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" }, "node_modules/@next/env": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.0.tgz", - "integrity": "sha512-4+70ELtSbRtYUuyRpAJmKC8NHBW2x1HMje9KO2Xd7IkoyucmV9SjgO+qeWMC0JWkRQXgydv1O7yKOK8nu/rITQ==" + "version": "14.2.2", + "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.2.tgz", + "integrity": "sha512-sk72qRfM1Q90XZWYRoJKu/UWlTgihrASiYw/scb15u+tyzcze3bOuJ/UV6TBOQEeUaxOkRqGeuGUdiiuxc5oqw==" }, "node_modules/@next/eslint-plugin-next": { "version": "14.1.0", @@ -1605,9 +1628,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.0.tgz", - "integrity": "sha512-kHktLlw0AceuDnkVljJ/4lTJagLzDiO3klR1Fzl2APDFZ8r+aTxNaNcPmpp0xLMkgRwwk6sggYeqq0Rz9K4zzA==", + "version": "14.2.2", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.2.tgz", + "integrity": "sha512-3iPgMhzbalizGwHNFUcGnDhFPSgVBHQ8aqSTAMxB5BvJG0oYrDf1WOJZlbXBgunOEj/8KMVbejEur/FpvFsgFQ==", "cpu": [ "arm64" ], @@ -1620,9 +1643,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.0.tgz", - "integrity": "sha512-HFSDu7lb1U3RDxXNeKH3NGRR5KyTPBSUTuIOr9jXoAso7i76gNYvnTjbuzGVWt2X5izpH908gmOYWtI7un+JrA==", + "version": "14.2.2", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.2.tgz", + "integrity": "sha512-x7Afi/jt0ZBRUZHTi49yyej4o8znfIMHO4RvThuoc0P+uli8Jd99y5GKjxoYunPKsXL09xBXEM1+OQy2xEL0Ag==", "cpu": [ "x64" ], @@ -1635,9 +1658,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.0.tgz", - "integrity": "sha512-iQsoWziO5ZMxDWZ4ZTCAc7hbJ1C9UDj/gATSqTaMjW2bJFwAsvf9UM79AKnljBl73uPZ+V0kH4rvnHTco4Ps2w==", + "version": "14.2.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.2.tgz", + "integrity": "sha512-zbfPtkk7L41ODMJwSp5VbmPozPmMMQrzAc0HAUomVeVIIwlDGs/UCqLJvLNDt4jpWgc21SjjyIn762lNGrMaUA==", "cpu": [ "arm64" ], @@ -1650,9 +1673,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.0.tgz", - "integrity": "sha512-0JOk2uzLUt8fJK5LpsKKZa74zAch7bJjjgJzR9aOMs231AlE4gPYzsSm430ckZitjPGKeH5bgDZjqwqJQKIS2w==", + "version": "14.2.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.2.tgz", + "integrity": "sha512-wPbS3pI/JU16rm3XdLvvTmlsmm1nd+sBa2ohXgBZcShX4TgOjD4R+RqHKlI1cjo/jDZKXt6OxmcU0Iys0OC/yg==", "cpu": [ "arm64" ], @@ -1665,9 +1688,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.0.tgz", - "integrity": "sha512-uYHkuTzX0NM6biKNp7hdKTf+BF0iMV254SxO0B8PgrQkxUBKGmk5ysHKB+FYBfdf9xei/t8OIKlXJs9ckD943A==", + "version": "14.2.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.2.tgz", + "integrity": "sha512-NqWOHqqq8iC9tuHvZxjQ2tX+jWy2X9y8NX2mcB4sj2bIccuCxbIZrU/ThFPZZPauygajZuVQ6zediejQHwZHwQ==", "cpu": [ "x64" ], @@ -1680,9 +1703,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.0.tgz", - "integrity": "sha512-paN89nLs2dTBDtfXWty1/NVPit+q6ldwdktixYSVwiiAz647QDCd+EIYqoiS+/rPG3oXs/A7rWcJK9HVqfnMVg==", + "version": "14.2.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.2.tgz", + "integrity": "sha512-lGepHhwb9sGhCcU7999+iK1ZZT+6rrIoVg40MP7DZski9GIZP80wORSbt5kJzh9v2x2ev2lxC6VgwMQT0PcgTA==", "cpu": [ "x64" ], @@ -1695,9 +1718,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.0.tgz", - "integrity": "sha512-j1oiidZisnymYjawFqEfeGNcE22ZQ7lGUaa4pGOCVWrWeIDkPSj8zYgS9TzMNlg17Q3wSWCQC/F5uJAhSh7qcA==", + "version": "14.2.2", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.2.tgz", + "integrity": "sha512-TZSh/48SfcLEQ4rD25VVn2kdIgUWmMflRX3OiyPwGNXn3NiyPqhqei/BaqCYXViIQ+6QsG9R0C8LftMqy8JPMA==", "cpu": [ "arm64" ], @@ -1710,9 +1733,9 @@ } }, "node_modules/@next/swc-win32-ia32-msvc": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.0.tgz", - "integrity": "sha512-6ff6F4xb+QGD1jhx/dOT9Ot7PQ/GAYekV9ykwEh2EFS/cLTyU4Y3cXkX5cNtNIhpctS5NvyjW9gIksRNErYE0A==", + "version": "14.2.2", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.2.tgz", + "integrity": "sha512-M0tBVNMEBJN2ZNQWlcekMn6pvLria7Sa2Fai5znm7CCJz4pP3lrvlSxhKdkCerk0D9E0bqx5yAo3o2Q7RrD4gA==", "cpu": [ "ia32" ], @@ -1725,9 +1748,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.0.tgz", - "integrity": "sha512-09DbG5vXAxz0eTFSf1uebWD36GF3D5toynRkgo2AlSrxwGZkWtJ1RhmrczRYQ17eD5bdo4FZ0ibiffdq5kc4vg==", + "version": "14.2.2", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.2.tgz", + "integrity": "sha512-a/20E/wtTJZ3Ykv3f/8F0l7TtgQa2LWHU2oNB9bsu0VjqGuGGHmm/q6waoUNQYTVPYrrlxxaHjJcDV6aiSTt/w==", "cpu": [ "x64" ], @@ -1844,6 +1867,133 @@ } } }, + "node_modules/@prettier/eslint/node_modules/@typescript-eslint/parser": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", + "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@prettier/eslint/node_modules/@typescript-eslint/scope-manager": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", + "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@prettier/eslint/node_modules/@typescript-eslint/types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", + "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", + "dev": true, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@prettier/eslint/node_modules/@typescript-eslint/typescript-estree": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", + "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@prettier/eslint/node_modules/@typescript-eslint/visitor-keys": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", + "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@prettier/eslint/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@prettier/eslint/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.14.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.14.0.tgz", @@ -2260,6 +2410,12 @@ "@types/node": "*" } }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", @@ -2317,36 +2473,68 @@ "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==" }, - "node_modules/@types/xml2js": { - "version": "0.4.14", - "resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.14.tgz", - "integrity": "sha512-4YnrRemBShWRO2QjvUin8ESA41rH+9nQGLUGZV/1IDhi3SL9OhdpNC/MrulTWuptXKwhx/aDxE7toV0f/ypIXQ==", + "node_modules/@types/semver": { + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", + "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", + "dev": true + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.7.0.tgz", + "integrity": "sha512-GJWR0YnfrKnsRoluVO3PRb9r5aMZriiMMM/RHj5nnTrBy1/wIgk76XCtCKcnXGjpZQJQRFtGV9/0JJ6n30uwpQ==", "dev": true, "dependencies": { - "@types/node": "*" + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "7.7.0", + "@typescript-eslint/type-utils": "7.7.0", + "@typescript-eslint/utils": "7.7.0", + "@typescript-eslint/visitor-keys": "7.7.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, "node_modules/@typescript-eslint/parser": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", - "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.7.0.tgz", + "integrity": "sha512-fNcDm3wSwVM8QYL4HKVBggdIPAy9Q41vcvC/GtDobw3c4ndVT3K6cqudUmjHPw8EAp4ufax0o58/xvWaP2FmTg==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/typescript-estree": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", + "@typescript-eslint/scope-manager": "7.7.0", + "@typescript-eslint/types": "7.7.0", + "@typescript-eslint/typescript-estree": "7.7.0", + "@typescript-eslint/visitor-keys": "7.7.0", "debug": "^4.3.4" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" + "eslint": "^8.56.0" }, "peerDependenciesMeta": { "typescript": { @@ -2355,29 +2543,56 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", - "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.7.0.tgz", + "integrity": "sha512-/8INDn0YLInbe9Wt7dK4cXLDYp0fNHP5xKLHvZl3mOT5X17rK/YShXaiNmorl+/U4VKCVIjJnx4Ri5b0y+HClw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0" + "@typescript-eslint/types": "7.7.0", + "@typescript-eslint/visitor-keys": "7.7.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/type-utils": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.7.0.tgz", + "integrity": "sha512-bOp3ejoRYrhAlnT/bozNQi3nio9tIgv3U5C0mVDdZC7cpcQEDZXvq8inrHYghLVwuNABRqrMW5tzAv88Vy77Sg==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "7.7.0", + "@typescript-eslint/utils": "7.7.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/@typescript-eslint/types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", - "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.7.0.tgz", + "integrity": "sha512-G01YPZ1Bd2hn+KPpIbrAhEWOn5lQBrjxkzHkWvP6NucMXFtfXoevK82hzQdpfuQYuhkvFDeQYbzXCjR1z9Z03w==", "dev": true, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -2385,22 +2600,22 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", - "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.7.0.tgz", + "integrity": "sha512-8p71HQPE6CbxIBy2kWHqM1KGrC07pk6RJn40n0DSc6bMOBBREZxSDJ+BmRzc8B5OdaMh1ty3mkuWRg4sCFiDQQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", + "@typescript-eslint/types": "7.7.0", + "@typescript-eslint/visitor-keys": "7.7.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", - "minimatch": "9.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -2422,9 +2637,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", "dev": true, "dependencies": { "brace-expansion": "^2.0.1" @@ -2436,17 +2651,42 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@typescript-eslint/utils": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.7.0.tgz", + "integrity": "sha512-LKGAXMPQs8U/zMRFXDZOzmMKgFv3COlxUQ+2NMPhbqgVm6R1w+nU1i4836Pmxu9jZAuIeyySNrN/6Rc657ggig==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.15", + "@types/semver": "^7.5.8", + "@typescript-eslint/scope-manager": "7.7.0", + "@typescript-eslint/types": "7.7.0", + "@typescript-eslint/typescript-estree": "7.7.0", + "semver": "^7.6.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", - "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.7.0.tgz", + "integrity": "sha512-h0WHOj8MhdhY8YWkzIF30R379y0NqyOHExI9N9KCzvmu05EgG4FumeYa3ccfKUSphyWkWQE1ybVrgz/Pbam6YA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.21.0", - "eslint-visitor-keys": "^3.4.1" + "@typescript-eslint/types": "7.7.0", + "eslint-visitor-keys": "^3.4.3" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -4075,6 +4315,133 @@ } } }, + "node_modules/eslint-config-next/node_modules/@typescript-eslint/parser": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", + "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-config-next/node_modules/@typescript-eslint/scope-manager": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", + "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-config-next/node_modules/@typescript-eslint/types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", + "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", + "dev": true, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-config-next/node_modules/@typescript-eslint/typescript-estree": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", + "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-config-next/node_modules/@typescript-eslint/visitor-keys": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", + "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-config-next/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/eslint-config-next/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/eslint-config-prettier": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", @@ -6454,11 +6821,11 @@ "dev": true }, "node_modules/next": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/next/-/next-14.2.0.tgz", - "integrity": "sha512-2T41HqJdKPqheR27ll7MFZ3gtTYvGew7cUc0PwPSyK9Ao5vvwpf9bYfP4V5YBGLckHF2kEGvrLte5BqLSv0s8g==", + "version": "14.2.2", + "resolved": "https://registry.npmjs.org/next/-/next-14.2.2.tgz", + "integrity": "sha512-oGwUaa2bCs47FbuxWMpOoXtBMPYpvTPgdZr3UAo+pu7Ns00z9otmYpoeV1HEiYL06AlRQQIA/ypK526KjJfaxg==", "dependencies": { - "@next/env": "14.2.0", + "@next/env": "14.2.2", "@swc/helpers": "0.5.5", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", @@ -6473,15 +6840,15 @@ "node": ">=18.17.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "14.2.0", - "@next/swc-darwin-x64": "14.2.0", - "@next/swc-linux-arm64-gnu": "14.2.0", - "@next/swc-linux-arm64-musl": "14.2.0", - "@next/swc-linux-x64-gnu": "14.2.0", - "@next/swc-linux-x64-musl": "14.2.0", - "@next/swc-win32-arm64-msvc": "14.2.0", - "@next/swc-win32-ia32-msvc": "14.2.0", - "@next/swc-win32-x64-msvc": "14.2.0" + "@next/swc-darwin-arm64": "14.2.2", + "@next/swc-darwin-x64": "14.2.2", + "@next/swc-linux-arm64-gnu": "14.2.2", + "@next/swc-linux-arm64-musl": "14.2.2", + "@next/swc-linux-x64-gnu": "14.2.2", + "@next/swc-linux-x64-musl": "14.2.2", + "@next/swc-win32-arm64-msvc": "14.2.2", + "@next/swc-win32-ia32-msvc": "14.2.2", + "@next/swc-win32-x64-msvc": "14.2.2" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", @@ -8113,9 +8480,9 @@ } }, "node_modules/ts-api-utils": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.2.1.tgz", - "integrity": "sha512-RIYA36cJn2WiH9Hy77hdF9r7oEwxAtB/TS9/S4Qd90Ap4z5FSiin5zEiTL44OII1Y3IIlEvxwxFUVgrHSZ/UpA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", "dev": true, "engines": { "node": ">=16" diff --git a/package.json b/package.json index a39426a9..0227f4d5 100644 --- a/package.json +++ b/package.json @@ -6,36 +6,39 @@ "dev": "touch .env.development.local && NODE_EXTRA_CA_CERTS=\"$(mkcert -CAROOT)/rootCA.pem\" node --env-file=.env --env-file=.env.development.local dev-server.mjs", "dev-test": "VIRKAILIJA_URL=http://localhost:3104 npm run dev", "build": "next build", - "start": "next start", + "start": "NODE_ENV=production npm run dev", "lint": "next lint", "prettier-eslint:fix": "prettier-eslint \"**/*.{js,ts,mjs,cjs,jsx,tsx}\" --write", "test:ui": "playwright test", "test": "vitest", "create-dev-certs": "mkdir -p certificates && cd certificates && mkcert localhost && mkcert -install", - "prepare": "husky" + "prepare": "husky", + "typecheck": "tsc" }, "dependencies": { "@emotion/cache": "^11.11.0", "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.0", "@mui/icons-material": "^5.15.15", - "@mui/material": "^5.15.14", + "@mui/material": "^5.15.15", "@mui/material-nextjs": "^5.15.11", "@tanstack/react-query": "^5.29.2", "@tanstack/react-query-devtools": "^5.29.2", "cookie": "^0.6.0", - "next": "^14.2.0", + "next": "^14.2.2", "react": "^18", "react-dom": "^18" }, "devDependencies": { + "@axe-core/playwright": "^4.9.0", "@playwright/test": "^1.42.0", "@testing-library/react": "^14.2.2", "@types/cookie": "^0.6.0", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", - "@types/xml2js": "^0.4.14", + "@typescript-eslint/eslint-plugin": "^7.7.0", + "@typescript-eslint/parser": "^7.7.0", "@vitejs/plugin-react": "^4.2.1", "autoprefixer": "^10.0.1", "eslint": "^8", diff --git a/src/app/@header/haku/[oid]/page.tsx b/src/app/@header/haku/[oid]/page.tsx new file mode 100644 index 00000000..7447d374 --- /dev/null +++ b/src/app/@header/haku/[oid]/page.tsx @@ -0,0 +1,14 @@ +'use client'; +import Header from '@/app/components/header'; +import { getTranslation } from '@/app/lib/common'; +import { getHaku } from '@/app/lib/kouta'; +import { useSuspenseQuery } from '@tanstack/react-query'; + +export default function HeaderPage({ params }: { params: { oid: string } }) { + const { data: hakuNimi } = useSuspenseQuery({ + queryKey: ['getHaku', params.oid], + queryFn: () => getHaku(params.oid), + }); + + return
; +} diff --git a/src/app/@header/loading.tsx b/src/app/@header/loading.tsx new file mode 100644 index 00000000..3bcaf47f --- /dev/null +++ b/src/app/@header/loading.tsx @@ -0,0 +1,6 @@ +import Header from '@/app/components/header'; +import { CircularProgress } from '@mui/material'; + +export default function Loading() { + return
} isHome={true} />; +} diff --git a/src/app/@header/page.tsx b/src/app/@header/page.tsx new file mode 100644 index 00000000..9cd82113 --- /dev/null +++ b/src/app/@header/page.tsx @@ -0,0 +1,5 @@ +import Header from '@/app/components/header'; + +export default function HakuHeaderPage() { + return
; +} diff --git a/src/app/components/error-boundary.tsx b/src/app/components/error-boundary.tsx new file mode 100644 index 00000000..19a5be32 --- /dev/null +++ b/src/app/components/error-boundary.tsx @@ -0,0 +1,32 @@ +'use client'; +import { useEffect } from 'react'; +import { FetchError } from '../lib/common'; + +export default function Error({ + error, + reset, +}: { + error: (Error & { digest?: string }) | FetchError; + reset: () => void; +}) { + useEffect(() => { + console.error(error); + }); + + if (error instanceof FetchError) { + return ( + <> +

Palvelinpyyntö epäonnistui!

+

Virhekoodi: {error.response.status}

+ + + ); + } else { + return ( + <> +

Jokin meni vikaan

+ + + ); + } +} diff --git a/src/app/components/full-spinner.tsx b/src/app/components/full-spinner.tsx new file mode 100644 index 00000000..53a57db3 --- /dev/null +++ b/src/app/components/full-spinner.tsx @@ -0,0 +1,17 @@ +import { Box, CircularProgress } from '@mui/material'; + +export const FullSpinner = () => ( + + + +); diff --git a/src/app/components/haku-filters.tsx b/src/app/components/haku-filters.tsx index 947caf52..af7f9ade 100644 --- a/src/app/components/haku-filters.tsx +++ b/src/app/components/haku-filters.tsx @@ -140,6 +140,7 @@ const HakuFiltersInternal = ({ Hae hakuja - Hakutapa + Hakutapa
{!isHome && ( - + diff --git a/src/app/components/table/list-table.tsx b/src/app/components/table/list-table.tsx index 39f08ded..c37dfabf 100644 --- a/src/app/components/table/list-table.tsx +++ b/src/app/components/table/list-table.tsx @@ -9,20 +9,26 @@ import { TableRow, styled, } from '@mui/material'; -import { getTranslation } from '@/app/lib/common'; +import { TranslatedName, getTranslation } from '@/app/lib/common'; import { Haku, getAlkamisKausi, Tila } from '@/app/lib/kouta-types'; -type Column = { +type Column

= { title?: string; key: string; - render: (obj: any) => React.ReactNode; + render: (props: P) => React.ReactNode; style?: Record; }; -export const makeHakuColumn = (): Column => ({ +type Entity = { oid: string; nimi: TranslatedName; tila: Tila }; + +type KeysMatching = { + [K in keyof O]: O[K] extends T ? K : never; +}[keyof O & string]; + +export const makeHakuColumn = (): Column => ({ title: 'Nimi', key: 'hakuNimi', - render: (haku: Haku) => ( + render: (haku) => ( {getTranslation(haku.nimi)} @@ -39,35 +45,35 @@ export const makeCountColumn = ({ }: { title: string; key: string; - amountProp: string; -}): Column => ({ + amountProp: KeysMatching; +}): Column => ({ title, key, - render: (props: any) => {props[amountProp] ?? 0}, + render: (props) => {props[amountProp] ?? 0}, style: { width: 0 }, }); -export const makeTilaColumn = (): Column => ({ +export const makeTilaColumn = (): Column => ({ title: 'Tila', key: 'tila', - render: (haku: Haku) => {Tila[haku.tila]}, + render: (haku) => {Tila[haku.tila]}, style: { width: 0, }, }); -export const makeHakutapaColumn = (getMatchingHakutapa: Function): Column => ({ +export const makeHakutapaColumn = ( + getMatchingHakutapa: (koodiUri: string) => string | undefined, +): Column => ({ title: 'Hakutapa', key: 'hakutapa', - render: (haku: Haku) => ( - {getMatchingHakutapa(haku.hakutapaKoodiUri)} - ), + render: (haku) => {getMatchingHakutapa(haku.hakutapaKoodiUri)}, }); -export const makeKoulutuksenAlkamiskausiColumn = (): Column => ({ +export const makeKoulutuksenAlkamiskausiColumn = (): Column => ({ title: 'Koulutuksen alkamiskausi', key: 'koulutuksenAlkamiskausi', - render: (haku: Haku) => ( + render: (haku) => ( {haku.alkamisKausiKoodiUri ? `${haku.alkamisVuosi} ${getAlkamisKausi(haku.alkamisKausiKoodiUri)}` @@ -76,11 +82,6 @@ export const makeKoulutuksenAlkamiskausiColumn = (): Column => ({ ), }); -type ListTableProps = { - columns?: Array; - rows?: Array; -}; - const StyledTable = styled(Table)({ width: '100%', borderSpacing: '0px', @@ -99,15 +100,20 @@ const StyledRow = styled(TableRow)({ backgroundColor: '#f5f5f5', }, '&:hover': { - backgroundColor: '#e0f2fd', + backgroundColor: '#e5f5ff', }, }); -export const ListTable = ({ +interface ListTableProps extends React.ComponentProps { + columns?: Array>; + rows?: Array; +} + +export const ListTable = ({ columns = [], rows = [], ...props -}: ListTableProps) => { +}: ListTableProps) => { return ( diff --git a/src/app/error.tsx b/src/app/error.tsx new file mode 100644 index 00000000..7b8f8d80 --- /dev/null +++ b/src/app/error.tsx @@ -0,0 +1,4 @@ +'use client'; +import ErrorBoundary from '@/app/components/error-boundary'; + +export default ErrorBoundary; diff --git a/src/app/global-error.tsx b/src/app/global-error.tsx new file mode 100644 index 00000000..7199d679 --- /dev/null +++ b/src/app/global-error.tsx @@ -0,0 +1,18 @@ +'use client'; +import ErrorBoundary from './components/error-boundary'; + +export default function GlobalError({ + error, + reset, +}: { + error: Error & { digest?: string }; + reset: () => void; +}) { + return ( + + + + + + ); +} diff --git a/src/app/haku/[oid]/error.tsx b/src/app/haku/[oid]/error.tsx new file mode 100644 index 00000000..7b8f8d80 --- /dev/null +++ b/src/app/haku/[oid]/error.tsx @@ -0,0 +1,4 @@ +'use client'; +import ErrorBoundary from '@/app/components/error-boundary'; + +export default ErrorBoundary; diff --git a/src/app/haku/[oid]/page.tsx b/src/app/haku/[oid]/page.tsx index f8b1f88c..97d5361b 100644 --- a/src/app/haku/[oid]/page.tsx +++ b/src/app/haku/[oid]/page.tsx @@ -1,35 +1,17 @@ 'use client'; -import Header from '@/app/components/header'; -import { getTranslation } from '@/app/lib/common'; -import { getHaku } from '@/app/lib/kouta'; -import { CircularProgress } from '@mui/material'; -import { useSuspenseQuery } from '@tanstack/react-query'; -import dynamic from 'next/dynamic'; - -const HakukohdeList = dynamic(() => import('./hakukohde-list'), { - ssr: false, - loading: () => , -}); +import HakukohdeList from './hakukohde-list'; export default function HakuPage({ params }: { params: { oid: string } }) { - const { data: hakuNimi } = useSuspenseQuery({ - queryKey: ['getHaku', params.oid], - queryFn: () => getHaku(params.oid), - }); - return ( -

-
-
- -
-

Valitse hakukohde

-

Kesken...

-
+
+ +
+

Valitse hakukohde

+

Kesken...

); diff --git a/src/app/layout.tsx b/src/app/layout.tsx index e18137cd..800d3b26 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -16,8 +16,10 @@ export const metadata: Metadata = { export default async function RootLayout({ children, + header, }: Readonly<{ children: React.ReactNode; + header: React.ReactNode; }>) { return ( @@ -25,7 +27,12 @@ export default async function RootLayout({ - {children} + + <> + {header} + {children} + + diff --git a/src/app/lib/common.ts b/src/app/lib/common.ts index e7d5136a..87680480 100644 --- a/src/app/lib/common.ts +++ b/src/app/lib/common.ts @@ -26,3 +26,13 @@ export function getTranslation( } return translated.sv || ''; } + +export class FetchError extends Error { + response: Response; + constructor(response: Response, message: string = 'Fetch error') { + super(message); + // Set the prototype explicitly. + Object.setPrototypeOf(this, FetchError.prototype); + this.response = response; + } +} diff --git a/src/app/lib/http-client.ts b/src/app/lib/http-client.ts index b225818b..316f5998 100644 --- a/src/app/lib/http-client.ts +++ b/src/app/lib/http-client.ts @@ -1,12 +1,13 @@ import { getCookies } from './cookie'; import { redirect } from 'next/navigation'; import { configuration } from './configuration'; +import { FetchError } from './common'; const doFetch = async (request: Request) => { try { const response = await fetch(request); return response.status >= 400 - ? Promise.reject(response) + ? Promise.reject(new FetchError(response)) : Promise.resolve(response); } catch (e) { return Promise.reject(e); @@ -29,7 +30,7 @@ const redirectToLogin = () => { const makeBareRequest = (request: Request) => { const { method } = request; - let modifiedOptions: RequestInit = { + const modifiedOptions: RequestInit = { headers: { 'Caller-id': '1.2.246.562.10.00000000001.valintojen-toteuttaminen', }, @@ -46,7 +47,7 @@ const makeBareRequest = (request: Request) => { return doFetch(new Request(request, modifiedOptions)); }; -const retryWithLogin = async (request: any, loginUrl: string) => { +const retryWithLogin = async (request: Request, loginUrl: string) => { await makeBareRequest(new Request(loginUrl)); return await makeBareRequest(request); }; @@ -67,7 +68,7 @@ const responseToData = async (res: Response) => { } }; -const makeRequest = async (request: Request) => { +const makeRequest = async (request: Request) => { try { const response = await makeBareRequest(request); @@ -79,8 +80,8 @@ const makeRequest = async (request: Request) => { return responseToData(response); } catch (error: unknown) { - if (error instanceof Response) { - if (isUnauthenticated(error)) { + if (error instanceof FetchError) { + if (isUnauthenticated(error.response)) { try { if (request?.url?.includes('/kouta-internal')) { const resp = await retryWithLogin( @@ -91,6 +92,9 @@ const makeRequest = async (request: Request) => { return responseToData(resp); } } catch (e) { + if (e instanceof FetchError && isUnauthenticated(e.response)) { + redirectToLogin(); + } return Promise.reject(e); } } diff --git a/src/app/lib/kouta.ts b/src/app/lib/kouta.ts index 8a332a68..cca3571c 100644 --- a/src/app/lib/kouta.ts +++ b/src/app/lib/kouta.ts @@ -5,7 +5,7 @@ import { TranslatedName } from './common'; import { Haku, Hakukohde, Tila } from './kouta-types'; import { client } from './http-client'; -export async function getHaut(active: boolean = true) { +export async function getHaut() { const response = await client.get(configuration.hautUrl); const haut: Haku[] = response.data.map( (h: { diff --git a/src/app/loading.tsx b/src/app/loading.tsx index fab71816..f485f5f6 100644 --- a/src/app/loading.tsx +++ b/src/app/loading.tsx @@ -1,5 +1,5 @@ -import { CircularProgress } from '@mui/material'; +import { FullSpinner } from './components/full-spinner'; export default function Loading() { - return ; + return ; } diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx index eaa83cd7..10f45ad3 100644 --- a/src/app/not-found.tsx +++ b/src/app/not-found.tsx @@ -2,47 +2,53 @@ import { Grid, Button, Typography } from '@mui/material'; export default function Custom404() { return ( - - - - 404 - - - - - Sivua ei löytynyt - - - Linkki on virheellinen tai vanhentunut. - - - - - - +
+ + + + 404 + + + + + Sivua ei löytynyt + + + Linkki on virheellinen tai vanhentunut. + + + + + + + - +
); } diff --git a/src/app/page.tsx b/src/app/page.tsx index 8cee438b..db3987ba 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,10 +1,8 @@ 'use server'; -import { CircularProgress } from '@mui/material'; -import dynamic from 'next/dynamic'; -import Header from './components/header'; import { getHakutavat } from './lib/koodisto'; import AccessTimeIcon from '@mui/icons-material/AccessTime'; import { CSSProperties } from 'react'; +import HakuFilters from './components/haku-filters'; const titleSectionStyle: CSSProperties = { borderBottom: '1px solid rgba(0, 0, 0, 0.15)', @@ -16,25 +14,17 @@ const titleSectionStyle: CSSProperties = { padding: 0, }; -const HakuFilters = dynamic(() => import('./components/haku-filters'), { - ssr: false, - loading: () => , -}); - export default async function Home() { const hakutavat = await getHakutavat(); return ( -
-
-
-
-

- Haut -

-
- +
+
+

+ Haut +

+
); } diff --git a/src/app/theme.tsx b/src/app/theme.tsx index bc0b713a..40b4460e 100644 --- a/src/app/theme.tsx +++ b/src/app/theme.tsx @@ -10,6 +10,12 @@ const LinkBehaviour = React.forwardRef( ); const theme = createTheme({ + palette: { + primary: { + main: '#0a789c', + contrastText: '#fff', + }, + }, components: { MuiLink: { defaultProps: { diff --git a/src/app/wrapper.tsx b/src/app/wrapper.tsx index c4e1c27d..18c792fd 100644 --- a/src/app/wrapper.tsx +++ b/src/app/wrapper.tsx @@ -1,13 +1,13 @@ 'use client'; +import { FullSpinner } from './components/full-spinner'; import { useAsiointiKieli } from './lib/hooks/useAsiointiKieli'; -import { CircularProgress } from '@mui/material'; export default function Wrapper({ children }: { children: React.ReactNode }) { const { isLoading, isError, error } = useAsiointiKieli(); switch (true) { case isLoading: - return ; + return ; case isError: throw error; default: diff --git a/tests/e2e/haut.spec.ts b/tests/e2e/haut.spec.ts index 8736f5ec..446bf781 100644 --- a/tests/e2e/haut.spec.ts +++ b/tests/e2e/haut.spec.ts @@ -1,4 +1,8 @@ import { test, expect, Page } from '@playwright/test'; +import { + expectAllSpinnersHidden, + expectPageAccessibilityOk, +} from './playwright-utils'; async function selectHakutapa(page: Page, idx: number, expectedOption: string) { await page.getByTestId('haku-hakutapa-select').click(); @@ -21,6 +25,14 @@ test.beforeEach(async ({ page }) => { await expect(page).toHaveTitle(/Valintojen Toteuttaminen/); }); +test('Haku-page accessibility', async ({ page }) => { + await page.goto( + '/valintojen-toteuttaminen/haku/1.2.246.562.29.00000000000000046872', + ); + await expectAllSpinnersHidden(page); + await expectPageAccessibilityOk(page); +}); + test('filters haku by published state', async ({ page }) => { await expect(page.getByTestId('haku-tila-toggle')).toContainText( 'Julkaistut', diff --git a/tests/e2e/index.spec.ts b/tests/e2e/index.spec.ts index 3ae15cb6..310223d0 100644 --- a/tests/e2e/index.spec.ts +++ b/tests/e2e/index.spec.ts @@ -1,10 +1,27 @@ import { test, expect } from '@playwright/test'; +import { + expectAllSpinnersHidden, + expectPageAccessibilityOk, +} from './playwright-utils'; + +test('index accessibility', async ({ page }) => { + await page.goto('/'); + await expectAllSpinnersHidden(page); + await page.locator('tr').nth(1).hover(); + await expectPageAccessibilityOk(page); +}); test('has title', async ({ page }) => { await page.goto('/'); await expect(page).toHaveTitle(/Valintojen Toteuttaminen/); }); +test('not found page accessibility', async ({ page }) => { + await page.goto('/valintojen-toteuttaminen/mimic-treasure-chest'); + await expectAllSpinnersHidden(page); + await expectPageAccessibilityOk(page); +}); + test('not found page', async ({ page }) => { await page.goto('/valintojen-toteuttaminen/mimic-treasure-chest'); await expect(page.locator('h1')).toContainText('404'); diff --git a/tests/e2e/playwright-utils.ts b/tests/e2e/playwright-utils.ts new file mode 100644 index 00000000..3181f600 --- /dev/null +++ b/tests/e2e/playwright-utils.ts @@ -0,0 +1,12 @@ +import AxeBuilder from '@axe-core/playwright'; +import { Page, expect } from '@playwright/test'; + +export const expectPageAccessibilityOk = async (page: Page) => { + const accessibilityScanResults = await new AxeBuilder({ page }).analyze(); + await expect(accessibilityScanResults.violations).toEqual([]); +}; + +export const expectAllSpinnersHidden = async (page: Page) => { + const spinners = page.getByRole('progressbar'); + await expect(spinners).toHaveCount(0); +}; diff --git a/tsconfig.json b/tsconfig.json index 7b285893..7f310556 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "noImplicitAny": true, "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true,