From dc96b1edbd9a1953ccb2356c369cdcc2dd6f6eeb Mon Sep 17 00:00:00 2001 From: Will Nilges Date: Sat, 3 May 2025 23:47:28 -0400 Subject: [PATCH 01/12] set up frontend --- frontend/cluesheet-web/.gitignore | 41 + frontend/cluesheet-web/README.md | 36 + frontend/cluesheet-web/next.config.ts | 7 + frontend/cluesheet-web/package-lock.json | 959 ++++++++++++++++++ frontend/cluesheet-web/package.json | 22 + frontend/cluesheet-web/public/file.svg | 1 + frontend/cluesheet-web/public/globe.svg | 1 + frontend/cluesheet-web/public/next.svg | 1 + frontend/cluesheet-web/public/vercel.svg | 1 + frontend/cluesheet-web/public/window.svg | 1 + frontend/cluesheet-web/src/app/favicon.ico | Bin 0 -> 25931 bytes frontend/cluesheet-web/src/app/globals.css | 42 + frontend/cluesheet-web/src/app/layout.tsx | 32 + .../cluesheet-web/src/app/page.module.css | 168 +++ frontend/cluesheet-web/src/app/page.tsx | 95 ++ frontend/cluesheet-web/tsconfig.json | 27 + 16 files changed, 1434 insertions(+) create mode 100644 frontend/cluesheet-web/.gitignore create mode 100644 frontend/cluesheet-web/README.md create mode 100644 frontend/cluesheet-web/next.config.ts create mode 100644 frontend/cluesheet-web/package-lock.json create mode 100644 frontend/cluesheet-web/package.json create mode 100644 frontend/cluesheet-web/public/file.svg create mode 100644 frontend/cluesheet-web/public/globe.svg create mode 100644 frontend/cluesheet-web/public/next.svg create mode 100644 frontend/cluesheet-web/public/vercel.svg create mode 100644 frontend/cluesheet-web/public/window.svg create mode 100644 frontend/cluesheet-web/src/app/favicon.ico create mode 100644 frontend/cluesheet-web/src/app/globals.css create mode 100644 frontend/cluesheet-web/src/app/layout.tsx create mode 100644 frontend/cluesheet-web/src/app/page.module.css create mode 100644 frontend/cluesheet-web/src/app/page.tsx create mode 100644 frontend/cluesheet-web/tsconfig.json diff --git a/frontend/cluesheet-web/.gitignore b/frontend/cluesheet-web/.gitignore new file mode 100644 index 0000000..5ef6a52 --- /dev/null +++ b/frontend/cluesheet-web/.gitignore @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/frontend/cluesheet-web/README.md b/frontend/cluesheet-web/README.md new file mode 100644 index 0000000..e215bc4 --- /dev/null +++ b/frontend/cluesheet-web/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/frontend/cluesheet-web/next.config.ts b/frontend/cluesheet-web/next.config.ts new file mode 100644 index 0000000..e9ffa30 --- /dev/null +++ b/frontend/cluesheet-web/next.config.ts @@ -0,0 +1,7 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + /* config options here */ +}; + +export default nextConfig; diff --git a/frontend/cluesheet-web/package-lock.json b/frontend/cluesheet-web/package-lock.json new file mode 100644 index 0000000..90e9ff8 --- /dev/null +++ b/frontend/cluesheet-web/package-lock.json @@ -0,0 +1,959 @@ +{ + "name": "cluesheet-web", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "cluesheet-web", + "version": "0.1.0", + "dependencies": { + "next": "15.3.1", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "typescript": "^5" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz", + "integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.1", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.1.tgz", + "integrity": "sha512-pn44xgBtgpEbZsu+lWf2KNb6OAf70X68k+yk69Ic2Xz11zHR/w24/U49XT7AeRwJ0Px+mhALhU5LPci1Aymk7A==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.1.0" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.1", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.1.tgz", + "integrity": "sha512-VfuYgG2r8BpYiOUN+BfYeFo69nP/MIwAtSJ7/Zpxc5QF3KS22z8Pvg3FkrSFJBPNQ7mmcUcYQFBmEQp7eu1F8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.1.0" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.1.0.tgz", + "integrity": "sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.1.0.tgz", + "integrity": "sha512-Xzc2ToEmHN+hfvsl9wja0RlnXEgpKNmftriQp6XzY/RaSfwD9th+MSh0WQKzUreLKKINb3afirxW7A0fz2YWuQ==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.1.0.tgz", + "integrity": "sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.1.0.tgz", + "integrity": "sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.1.0.tgz", + "integrity": "sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.1.0.tgz", + "integrity": "sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.1.0.tgz", + "integrity": "sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.1.0.tgz", + "integrity": "sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.1.0.tgz", + "integrity": "sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.1", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.1.tgz", + "integrity": "sha512-anKiszvACti2sGy9CirTlNyk7BjjZPiML1jt2ZkTdcvpLU1YH6CXwRAZCA2UmRXnhiIftXQ7+Oh62Ji25W72jA==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.1.0" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.1", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.1.tgz", + "integrity": "sha512-kX2c+vbvaXC6vly1RDf/IWNXxrlxLNpBVWkdpRq5Ka7OOKj6nr66etKy2IENf6FtOgklkg9ZdGpEu9kwdlcwOQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.1.0" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.1", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.1.tgz", + "integrity": "sha512-7s0KX2tI9mZI2buRipKIw2X1ufdTeaRgwmRabt5bi9chYfhur+/C1OXg3TKg/eag1W+6CCWLVmSauV1owmRPxA==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.1.0" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.1", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.1.tgz", + "integrity": "sha512-wExv7SH9nmoBW3Wr2gvQopX1k8q2g5V5Iag8Zk6AVENsjwd+3adjwxtp3Dcu2QhOXr8W9NusBU6XcQUohBZ5MA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.1.0" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.1", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.1.tgz", + "integrity": "sha512-DfvyxzHxw4WGdPiTF0SOHnm11Xv4aQexvqhRDAoD00MzHekAj9a/jADXeXYCDFH/DzYruwHbXU7uz+H+nWmSOQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.1.0" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.1", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.1.tgz", + "integrity": "sha512-pax/kTR407vNb9qaSIiWVnQplPcGU8LRIJpDT5o8PdAx5aAA7AS3X9PS8Isw1/WfqgQorPotjrZL3Pqh6C5EBg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.1.0" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.1", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.1.tgz", + "integrity": "sha512-YDybQnYrLQfEpzGOQe7OKcyLUCML4YOXl428gOOzBgN6Gw0rv8dpsJ7PqTHxBnXnwXr8S1mYFSLSa727tpz0xg==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.4.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.1", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.1.tgz", + "integrity": "sha512-WKf/NAZITnonBf3U1LfdjoMgNO5JYRSlhovhRhMxXVdvWYveM4kM3L8m35onYIdh75cOMCo1BexgVQcCDzyoWw==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.1", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.1.tgz", + "integrity": "sha512-hw1iIAHpNE8q3uMIRCgGOeDoz9KtFNarFLQclLxr/LK1VBkj8nby18RjFvr6aP7USRYAjTZW6yisnBWMX571Tw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@next/env": { + "version": "15.3.1", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.3.1.tgz", + "integrity": "sha512-cwK27QdzrMblHSn9DZRV+DQscHXRuJv6MydlJRpFSqJWZrTYMLzKDeyueJNN9MGd8NNiUKzDQADAf+dMLXX7YQ==", + "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "15.3.1", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.3.1.tgz", + "integrity": "sha512-hjDw4f4/nla+6wysBL07z52Gs55Gttp5Bsk5/8AncQLJoisvTBP0pRIBK/B16/KqQyH+uN4Ww8KkcAqJODYH3w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "15.3.1", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.3.1.tgz", + "integrity": "sha512-q+aw+cJ2ooVYdCEqZVk+T4Ni10jF6Fo5DfpEV51OupMaV5XL6pf3GCzrk6kSSZBsMKZtVC1Zm/xaNBFpA6bJ2g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.3.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.3.1.tgz", + "integrity": "sha512-wBQ+jGUI3N0QZyWmmvRHjXjTWFy8o+zPFLSOyAyGFI94oJi+kK/LIZFJXeykvgXUk1NLDAEFDZw/NVINhdk9FQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.3.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.3.1.tgz", + "integrity": "sha512-IIxXEXRti/AulO9lWRHiCpUUR8AR/ZYLPALgiIg/9ENzMzLn3l0NSxVdva7R/VDcuSEBo0eGVCe3evSIHNz0Hg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "15.3.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.3.1.tgz", + "integrity": "sha512-bfI4AMhySJbyXQIKH5rmLJ5/BP7bPwuxauTvVEiJ/ADoddaA9fgyNNCcsbu9SlqfHDoZmfI6g2EjzLwbsVTr5A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "15.3.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.3.1.tgz", + "integrity": "sha512-FeAbR7FYMWR+Z+M5iSGytVryKHiAsc0x3Nc3J+FD5NVbD5Mqz7fTSy8CYliXinn7T26nDMbpExRUI/4ekTvoiA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.3.1", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.3.1.tgz", + "integrity": "sha512-yP7FueWjphQEPpJQ2oKmshk/ppOt+0/bB8JC8svPUZNy0Pi3KbPx2Llkzv1p8CoQa+D2wknINlJpHf3vtChVBw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.3.1", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.3.1.tgz", + "integrity": "sha512-3PMvF2zRJAifcRNni9uMk/gulWfWS+qVI/pagd+4yLF5bcXPZPPH2xlYRYOsUjmCJOXSTAC2PjRzbhsRzR2fDQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "license": "Apache-2.0" + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@types/node": { + "version": "20.17.32", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.32.tgz", + "integrity": "sha512-zeMXFn8zQ+UkjK4ws0RiOC9EWByyW1CcVmLe+2rQocXRsGEDxUCwPEIVgpsGcLHS/P8JkT0oa3839BRABS0oPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/@types/react": { + "version": "19.1.2", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.2.tgz", + "integrity": "sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.1.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.3.tgz", + "integrity": "sha512-rJXC08OG0h3W6wDMFxQrZF00Kq6qQvw0djHRdzl3U5DnIERz0MRce3WVc7IS6JYBwtaP/DwYtRRjVlvivNveKg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.0.0" + } + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001716", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001716.tgz", + "integrity": "sha512-49/c1+x3Kwz7ZIWt+4DvK3aMJy9oYXXG6/97JKsnjdCk/6n9vVyWL8NAwVt95Lwt9eigI10Hl782kDfZUUlRXw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", + "optional": true, + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT", + "optional": true + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "optional": true, + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT", + "optional": true + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/next": { + "version": "15.3.1", + "resolved": "https://registry.npmjs.org/next/-/next-15.3.1.tgz", + "integrity": "sha512-8+dDV0xNLOgHlyBxP1GwHGVaNXsmp+2NhZEYrXr24GWLHtt27YrBPbPuHvzlhi7kZNYjeJNR93IF5zfFu5UL0g==", + "license": "MIT", + "dependencies": { + "@next/env": "15.3.1", + "@swc/counter": "0.1.3", + "@swc/helpers": "0.5.15", + "busboy": "1.6.0", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "15.3.1", + "@next/swc-darwin-x64": "15.3.1", + "@next/swc-linux-arm64-gnu": "15.3.1", + "@next/swc-linux-arm64-musl": "15.3.1", + "@next/swc-linux-x64-gnu": "15.3.1", + "@next/swc-linux-x64-musl": "15.3.1", + "@next/swc-win32-arm64-msvc": "15.3.1", + "@next/swc-win32-x64-msvc": "15.3.1", + "sharp": "^0.34.1" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.41.2", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", + "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", + "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.26.0" + }, + "peerDependencies": { + "react": "^19.1.0" + } + }, + "node_modules/scheduler": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.34.1", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.1.tgz", + "integrity": "sha512-1j0w61+eVxu7DawFJtnfYcvSv6qPFvfTaqzTQ2BLknVhHTwGS8sc63ZBF4rzkWMBVKybo4S5OBtDdZahh2A1xg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.3", + "semver": "^7.7.1" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.1", + "@img/sharp-darwin-x64": "0.34.1", + "@img/sharp-libvips-darwin-arm64": "1.1.0", + "@img/sharp-libvips-darwin-x64": "1.1.0", + "@img/sharp-libvips-linux-arm": "1.1.0", + "@img/sharp-libvips-linux-arm64": "1.1.0", + "@img/sharp-libvips-linux-ppc64": "1.1.0", + "@img/sharp-libvips-linux-s390x": "1.1.0", + "@img/sharp-libvips-linux-x64": "1.1.0", + "@img/sharp-libvips-linuxmusl-arm64": "1.1.0", + "@img/sharp-libvips-linuxmusl-x64": "1.1.0", + "@img/sharp-linux-arm": "0.34.1", + "@img/sharp-linux-arm64": "0.34.1", + "@img/sharp-linux-s390x": "0.34.1", + "@img/sharp-linux-x64": "0.34.1", + "@img/sharp-linuxmusl-arm64": "0.34.1", + "@img/sharp-linuxmusl-x64": "0.34.1", + "@img/sharp-wasm32": "0.34.1", + "@img/sharp-win32-ia32": "0.34.1", + "@img/sharp-win32-x64": "0.34.1" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", + "optional": true, + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/frontend/cluesheet-web/package.json b/frontend/cluesheet-web/package.json new file mode 100644 index 0000000..4ec6352 --- /dev/null +++ b/frontend/cluesheet-web/package.json @@ -0,0 +1,22 @@ +{ + "name": "cluesheet-web", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev --turbopack", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "react": "^19.0.0", + "react-dom": "^19.0.0", + "next": "15.3.1" + }, + "devDependencies": { + "typescript": "^5", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19" + } +} diff --git a/frontend/cluesheet-web/public/file.svg b/frontend/cluesheet-web/public/file.svg new file mode 100644 index 0000000..004145c --- /dev/null +++ b/frontend/cluesheet-web/public/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/cluesheet-web/public/globe.svg b/frontend/cluesheet-web/public/globe.svg new file mode 100644 index 0000000..567f17b --- /dev/null +++ b/frontend/cluesheet-web/public/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/cluesheet-web/public/next.svg b/frontend/cluesheet-web/public/next.svg new file mode 100644 index 0000000..5174b28 --- /dev/null +++ b/frontend/cluesheet-web/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/cluesheet-web/public/vercel.svg b/frontend/cluesheet-web/public/vercel.svg new file mode 100644 index 0000000..7705396 --- /dev/null +++ b/frontend/cluesheet-web/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/cluesheet-web/public/window.svg b/frontend/cluesheet-web/public/window.svg new file mode 100644 index 0000000..b2b2a44 --- /dev/null +++ b/frontend/cluesheet-web/public/window.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/cluesheet-web/src/app/favicon.ico b/frontend/cluesheet-web/src/app/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..718d6fea4835ec2d246af9800eddb7ffb276240c GIT binary patch literal 25931 zcmeHv30#a{`}aL_*G&7qml|y<+KVaDM2m#dVr!KsA!#An?kSQM(q<_dDNCpjEux83 zLb9Z^XxbDl(w>%i@8hT6>)&Gu{h#Oeyszu?xtw#Zb1mO{pgX9699l+Qppw7jXaYf~-84xW z)w4x8?=youko|}Vr~(D$UXIbiXABHh`p1?nn8Po~fxRJv}|0e(BPs|G`(TT%kKVJAdg5*Z|x0leQq0 zkdUBvb#>9F()jo|T~kx@OM8$9wzs~t2l;K=woNssA3l6|sx2r3+kdfVW@e^8e*E}v zA1y5{bRi+3Z`uD3{F7LgFJDdvm;nJilkzDku>BwXH(8ItVCXk*-lSJnR?-2UN%hJ){&rlvg`CDTj z)Bzo!3v7Ou#83zEDEFcKt(f1E0~=rqeEbTnMvWR#{+9pg%7G8y>u1OVRUSoox-ovF z2Ydma(;=YuBY(eI|04{hXzZD6_f(v~H;C~y5=DhAC{MMS>2fm~1H_t2$56pc$NH8( z5bH|<)71dV-_oCHIrzrT`2s-5w_+2CM0$95I6X8p^r!gHp+j_gd;9O<1~CEQQGS8) zS9Qh3#p&JM-G8rHekNmKVewU;pJRcTAog68KYo^dRo}(M>36U4Us zfgYWSiHZL3;lpWT=zNAW>Dh#mB!_@Lg%$ms8N-;aPqMn+C2HqZgz&9~Eu z4|Kp<`$q)Uw1R?y(~S>ePdonHxpV1#eSP1B;Ogo+-Pk}6#0GsZZ5!||ev2MGdh}_m z{DeR7?0-1^zVs&`AV6Vt;r3`I`OI_wgs*w=eO%_#7Kepl{B@xiyCANc(l zzIyd4y|c6PXWq9-|KM8(zIk8LPk(>a)zyFWjhT!$HJ$qX1vo@d25W<fvZQ2zUz5WRc(UnFMKHwe1| zWmlB1qdbiA(C0jmnV<}GfbKtmcu^2*P^O?MBLZKt|As~ge8&AAO~2K@zbXelK|4T<{|y4`raF{=72kC2Kn(L4YyenWgrPiv z@^mr$t{#X5VuIMeL!7Ab6_kG$&#&5p*Z{+?5U|TZ`B!7llpVmp@skYz&n^8QfPJzL z0G6K_OJM9x+Wu2gfN45phANGt{7=C>i34CV{Xqlx(fWpeAoj^N0Biu`w+MVcCUyU* zDZuzO0>4Z6fbu^T_arWW5n!E45vX8N=bxTVeFoep_G#VmNlQzAI_KTIc{6>c+04vr zx@W}zE5JNSU>!THJ{J=cqjz+4{L4A{Ob9$ZJ*S1?Ggg3klFp!+Y1@K+pK1DqI|_gq z5ZDXVpge8-cs!o|;K73#YXZ3AShj50wBvuq3NTOZ`M&qtjj#GOFfgExjg8Gn8>Vq5 z`85n+9|!iLCZF5$HJ$Iu($dm?8~-ofu}tEc+-pyke=3!im#6pk_Wo8IA|fJwD&~~F zc16osQ)EBo58U7XDuMexaPRjU@h8tXe%S{fA0NH3vGJFhuyyO!Uyl2^&EOpX{9As0 zWj+P>{@}jxH)8|r;2HdupP!vie{sJ28b&bo!8`D^x}TE$%zXNb^X1p@0PJ86`dZyj z%ce7*{^oo+6%&~I!8hQy-vQ7E)0t0ybH4l%KltWOo~8cO`T=157JqL(oq_rC%ea&4 z2NcTJe-HgFjNg-gZ$6!Y`SMHrlj}Etf7?r!zQTPPSv}{so2e>Fjs1{gzk~LGeesX%r(Lh6rbhSo_n)@@G-FTQy93;l#E)hgP@d_SGvyCp0~o(Y;Ee8{ zdVUDbHm5`2taPUOY^MAGOw*>=s7=Gst=D+p+2yON!0%Hk` zz5mAhyT4lS*T3LS^WSxUy86q&GnoHxzQ6vm8)VS}_zuqG?+3td68_x;etQAdu@sc6 zQJ&5|4(I?~3d-QOAODHpZ=hlSg(lBZ!JZWCtHHSj`0Wh93-Uk)_S%zsJ~aD>{`A0~ z9{AG(e|q3g5B%wYKRxiL2Y$8(4w6bzchKuloQW#e&S3n+P- z8!ds-%f;TJ1>)v)##>gd{PdS2Oc3VaR`fr=`O8QIO(6(N!A?pr5C#6fc~Ge@N%Vvu zaoAX2&(a6eWy_q&UwOhU)|P3J0Qc%OdhzW=F4D|pt0E4osw;%<%Dn58hAWD^XnZD= z>9~H(3bmLtxpF?a7su6J7M*x1By7YSUbxGi)Ot0P77`}P3{)&5Un{KD?`-e?r21!4vTTnN(4Y6Lin?UkSM z`MXCTC1@4A4~mvz%Rh2&EwY))LeoT=*`tMoqcEXI>TZU9WTP#l?uFv+@Dn~b(>xh2 z;>B?;Tz2SR&KVb>vGiBSB`@U7VIWFSo=LDSb9F{GF^DbmWAfpms8Sx9OX4CnBJca3 zlj9(x!dIjN?OG1X4l*imJNvRCk}F%!?SOfiOq5y^mZW)jFL@a|r-@d#f7 z2gmU8L3IZq0ynIws=}~m^#@&C%J6QFo~Mo4V`>v7MI-_!EBMMtb%_M&kvAaN)@ZVw z+`toz&WG#HkWDjnZE!6nk{e-oFdL^$YnbOCN}JC&{$#$O27@|Tn-skXr)2ml2~O!5 zX+gYoxhoc7qoU?C^3~&!U?kRFtnSEecWuH0B0OvLodgUAi}8p1 zrO6RSXHH}DMc$&|?D004DiOVMHV8kXCP@7NKB zgaZq^^O<7PoKEp72kby@W0Z!Y*Ay{&vfg#C&gG@YVR9g?FEocMUi1gSN$+V+ayF45{a zuDZDTN}mS|;BO%gEf}pjBfN2-gIrU#G5~cucA;dokXW89%>AyXJJI z9X4UlIWA|ZYHgbI z5?oFk@A=Ik7lrEQPDH!H+b`7_Y~aDb_qa=B2^Y&Ow41cU=4WDd40dp5(QS-WMN-=Y z9g;6_-JdNU;|6cPwf$ak*aJIcwL@1n$#l~zi{c{EW?T;DaW*E8DYq?Umtz{nJ&w-M zEMyTDrC&9K$d|kZe2#ws6)L=7K+{ zQw{XnV6UC$6-rW0emqm8wJoeZK)wJIcV?dST}Z;G0Arq{dVDu0&4kd%N!3F1*;*pW zR&qUiFzK=@44#QGw7k1`3t_d8&*kBV->O##t|tonFc2YWrL7_eqg+=+k;!F-`^b8> z#KWCE8%u4k@EprxqiV$VmmtiWxDLgnGu$Vs<8rppV5EajBXL4nyyZM$SWVm!wnCj-B!Wjqj5-5dNXukI2$$|Bu3Lrw}z65Lc=1G z^-#WuQOj$hwNGG?*CM_TO8Bg-1+qc>J7k5c51U8g?ZU5n?HYor;~JIjoWH-G>AoUP ztrWWLbRNqIjW#RT*WqZgPJXU7C)VaW5}MiijYbABmzoru6EmQ*N8cVK7a3|aOB#O& zBl8JY2WKfmj;h#Q!pN%9o@VNLv{OUL?rixHwOZuvX7{IJ{(EdPpuVFoQqIOa7giLVkBOKL@^smUA!tZ1CKRK}#SSM)iQHk)*R~?M!qkCruaS!#oIL1c z?J;U~&FfH#*98^G?i}pA{ z9Jg36t4=%6mhY(quYq*vSxptes9qy|7xSlH?G=S@>u>Ebe;|LVhs~@+06N<4CViBk zUiY$thvX;>Tby6z9Y1edAMQaiH zm^r3v#$Q#2T=X>bsY#D%s!bhs^M9PMAcHbCc0FMHV{u-dwlL;a1eJ63v5U*?Q_8JO zT#50!RD619#j_Uf))0ooADz~*9&lN!bBDRUgE>Vud-i5ck%vT=r^yD*^?Mp@Q^v+V zG#-?gKlr}Eeqifb{|So?HM&g91P8|av8hQoCmQXkd?7wIJwb z_^v8bbg`SAn{I*4bH$u(RZ6*xUhuA~hc=8czK8SHEKTzSxgbwi~9(OqJB&gwb^l4+m`k*Q;_?>Y-APi1{k zAHQ)P)G)f|AyjSgcCFps)Fh6Bca*Xznq36!pV6Az&m{O8$wGFD? zY&O*3*J0;_EqM#jh6^gMQKpXV?#1?>$ml1xvh8nSN>-?H=V;nJIwB07YX$e6vLxH( zqYwQ>qxwR(i4f)DLd)-$P>T-no_c!LsN@)8`e;W@)-Hj0>nJ-}Kla4-ZdPJzI&Mce zv)V_j;(3ERN3_@I$N<^|4Lf`B;8n+bX@bHbcZTopEmDI*Jfl)-pFDvo6svPRoo@(x z);_{lY<;);XzT`dBFpRmGrr}z5u1=pC^S-{ce6iXQlLGcItwJ^mZx{m$&DA_oEZ)B{_bYPq-HA zcH8WGoBG(aBU_j)vEy+_71T34@4dmSg!|M8Vf92Zj6WH7Q7t#OHQqWgFE3ARt+%!T z?oLovLVlnf?2c7pTc)~cc^($_8nyKwsN`RA-23ed3sdj(ys%pjjM+9JrctL;dy8a( z@en&CQmnV(()bu|Y%G1-4a(6x{aLytn$T-;(&{QIJB9vMox11U-1HpD@d(QkaJdEb zG{)+6Dos_L+O3NpWo^=gR?evp|CqEG?L&Ut#D*KLaRFOgOEK(Kq1@!EGcTfo+%A&I z=dLbB+d$u{sh?u)xP{PF8L%;YPPW53+@{>5W=Jt#wQpN;0_HYdw1{ksf_XhO4#2F= zyPx6Lx2<92L-;L5PD`zn6zwIH`Jk($?Qw({erA$^bC;q33hv!d!>%wRhj# zal^hk+WGNg;rJtb-EB(?czvOM=H7dl=vblBwAv>}%1@{}mnpUznfq1cE^sgsL0*4I zJ##!*B?=vI_OEVis5o+_IwMIRrpQyT_Sq~ZU%oY7c5JMIADzpD!Upz9h@iWg_>>~j zOLS;wp^i$-E?4<_cp?RiS%Rd?i;f*mOz=~(&3lo<=@(nR!_Rqiprh@weZlL!t#NCc zO!QTcInq|%#>OVgobj{~ixEUec`E25zJ~*DofsQdzIa@5^nOXj2T;8O`l--(QyU^$t?TGY^7#&FQ+2SS3B#qK*k3`ye?8jUYSajE5iBbJls75CCc(m3dk{t?- zopcER9{Z?TC)mk~gpi^kbbu>b-+a{m#8-y2^p$ka4n60w;Sc2}HMf<8JUvhCL0B&Btk)T`ctE$*qNW8L$`7!r^9T+>=<=2qaq-;ll2{`{Rg zc5a0ZUI$oG&j-qVOuKa=*v4aY#IsoM+1|c4Z)<}lEDvy;5huB@1RJPquU2U*U-;gu z=En2m+qjBzR#DEJDO`WU)hdd{Vj%^0V*KoyZ|5lzV87&g_j~NCjwv0uQVqXOb*QrQ zy|Qn`hxx(58c70$E;L(X0uZZ72M1!6oeg)(cdKO ze0gDaTz+ohR-#d)NbAH4x{I(21yjwvBQfmpLu$)|m{XolbgF!pmsqJ#D}(ylp6uC> z{bqtcI#hT#HW=wl7>p!38sKsJ`r8}lt-q%Keqy%u(xk=yiIJiUw6|5IvkS+#?JTBl z8H5(Q?l#wzazujH!8o>1xtn8#_w+397*_cy8!pQGP%K(Ga3pAjsaTbbXJlQF_+m+-UpUUent@xM zg%jqLUExj~o^vQ3Gl*>wh=_gOr2*|U64_iXb+-111aH}$TjeajM+I20xw(((>fej-@CIz4S1pi$(#}P7`4({6QS2CaQS4NPENDp>sAqD z$bH4KGzXGffkJ7R>V>)>tC)uax{UsN*dbeNC*v}#8Y#OWYwL4t$ePR?VTyIs!wea+ z5Urmc)X|^`MG~*dS6pGSbU+gPJoq*^a=_>$n4|P^w$sMBBy@f*Z^Jg6?n5?oId6f{ z$LW4M|4m502z0t7g<#Bx%X;9<=)smFolV&(V^(7Cv2-sxbxopQ!)*#ZRhTBpx1)Fc zNm1T%bONzv6@#|dz(w02AH8OXe>kQ#1FMCzO}2J_mST)+ExmBr9cva-@?;wnmWMOk z{3_~EX_xadgJGv&H@zK_8{(x84`}+c?oSBX*Ge3VdfTt&F}yCpFP?CpW+BE^cWY0^ zb&uBN!Ja3UzYHK-CTyA5=L zEMW{l3Usky#ly=7px648W31UNV@K)&Ub&zP1c7%)`{);I4b0Q<)B}3;NMG2JH=X$U zfIW4)4n9ZM`-yRj67I)YSLDK)qfUJ_ij}a#aZN~9EXrh8eZY2&=uY%2N0UFF7<~%M zsB8=erOWZ>Ct_#^tHZ|*q`H;A)5;ycw*IcmVxi8_0Xk}aJA^ath+E;xg!x+As(M#0=)3!NJR6H&9+zd#iP(m0PIW8$ z1Y^VX`>jm`W!=WpF*{ioM?C9`yOR>@0q=u7o>BP-eSHqCgMDj!2anwH?s%i2p+Q7D zzszIf5XJpE)IG4;d_(La-xenmF(tgAxK`Y4sQ}BSJEPs6N_U2vI{8=0C_F?@7<(G; zo$~G=8p+076G;`}>{MQ>t>7cm=zGtfbdDXm6||jUU|?X?CaE?(<6bKDYKeHlz}DA8 zXT={X=yp_R;HfJ9h%?eWvQ!dRgz&Su*JfNt!Wu>|XfU&68iRikRrHRW|ZxzRR^`eIGt zIeiDgVS>IeExKVRWW8-=A=yA`}`)ZkWBrZD`hpWIxBGkh&f#ijr449~m`j6{4jiJ*C!oVA8ZC?$1RM#K(_b zL9TW)kN*Y4%^-qPpMP7d4)o?Nk#>aoYHT(*g)qmRUb?**F@pnNiy6Fv9rEiUqD(^O zzyS?nBrX63BTRYduaG(0VVG2yJRe%o&rVrLjbxTaAFTd8s;<<@Qs>u(<193R8>}2_ zuwp{7;H2a*X7_jryzriZXMg?bTuegABb^87@SsKkr2)0Gyiax8KQWstw^v#ix45EVrcEhr>!NMhprl$InQMzjSFH54x5k9qHc`@9uKQzvL4ihcq{^B zPrVR=o_ic%Y>6&rMN)hTZsI7I<3&`#(nl+3y3ys9A~&^=4?PL&nd8)`OfG#n zwAMN$1&>K++c{^|7<4P=2y(B{jJsQ0a#U;HTo4ZmWZYvI{+s;Td{Yzem%0*k#)vjpB zia;J&>}ICate44SFYY3vEelqStQWFihx%^vQ@Do(sOy7yR2@WNv7Y9I^yL=nZr3mb zXKV5t@=?-Sk|b{XMhA7ZGB@2hqsx}4xwCW!in#C zI@}scZlr3-NFJ@NFaJlhyfcw{k^vvtGl`N9xSo**rDW4S}i zM9{fMPWo%4wYDG~BZ18BD+}h|GQKc-g^{++3MY>}W_uq7jGHx{mwE9fZiPCoxN$+7 zrODGGJrOkcPQUB(FD5aoS4g~7#6NR^ma7-!>mHuJfY5kTe6PpNNKC9GGRiu^L31uG z$7v`*JknQHsYB!Tm_W{a32TM099djW%5e+j0Ve_ct}IM>XLF1Ap+YvcrLV=|CKo6S zb+9Nl3_YdKP6%Cxy@6TxZ>;4&nTneadr z_ES90ydCev)LV!dN=#(*f}|ZORFdvkYBni^aLbUk>BajeWIOcmHP#8S)*2U~QKI%S zyrLmtPqb&TphJ;>yAxri#;{uyk`JJqODDw%(Z=2`1uc}br^V%>j!gS)D*q*f_-qf8&D;W1dJgQMlaH5er zN2U<%Smb7==vE}dDI8K7cKz!vs^73o9f>2sgiTzWcwY|BMYHH5%Vn7#kiw&eItCqa zIkR2~Q}>X=Ar8W|^Ms41Fm8o6IB2_j60eOeBB1Br!boW7JnoeX6Gs)?7rW0^5psc- zjS16yb>dFn>KPOF;imD}e!enuIniFzv}n$m2#gCCv4jM#ArwlzZ$7@9&XkFxZ4n!V zj3dyiwW4Ki2QG{@i>yuZXQizw_OkZI^-3otXC{!(lUpJF33gI60ak;Uqitp74|B6I zgg{b=Iz}WkhCGj1M=hu4#Aw173YxIVbISaoc z-nLZC*6Tgivd5V`K%GxhBsp@SUU60-rfc$=wb>zdJzXS&-5(NRRodFk;Kxk!S(O(a0e7oY=E( zAyS;Ow?6Q&XA+cnkCb{28_1N8H#?J!*$MmIwLq^*T_9-z^&UE@A(z9oGYtFy6EZef LrJugUA?W`A8`#=m literal 0 HcmV?d00001 diff --git a/frontend/cluesheet-web/src/app/globals.css b/frontend/cluesheet-web/src/app/globals.css new file mode 100644 index 0000000..e3734be --- /dev/null +++ b/frontend/cluesheet-web/src/app/globals.css @@ -0,0 +1,42 @@ +:root { + --background: #ffffff; + --foreground: #171717; +} + +@media (prefers-color-scheme: dark) { + :root { + --background: #0a0a0a; + --foreground: #ededed; + } +} + +html, +body { + max-width: 100vw; + overflow-x: hidden; +} + +body { + color: var(--foreground); + background: var(--background); + font-family: Arial, Helvetica, sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +* { + box-sizing: border-box; + padding: 0; + margin: 0; +} + +a { + color: inherit; + text-decoration: none; +} + +@media (prefers-color-scheme: dark) { + html { + color-scheme: dark; + } +} diff --git a/frontend/cluesheet-web/src/app/layout.tsx b/frontend/cluesheet-web/src/app/layout.tsx new file mode 100644 index 0000000..42fc323 --- /dev/null +++ b/frontend/cluesheet-web/src/app/layout.tsx @@ -0,0 +1,32 @@ +import type { Metadata } from "next"; +import { Geist, Geist_Mono } from "next/font/google"; +import "./globals.css"; + +const geistSans = Geist({ + variable: "--font-geist-sans", + subsets: ["latin"], +}); + +const geistMono = Geist_Mono({ + variable: "--font-geist-mono", + subsets: ["latin"], +}); + +export const metadata: Metadata = { + title: "Create Next App", + description: "Generated by create next app", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + {children} + + + ); +} diff --git a/frontend/cluesheet-web/src/app/page.module.css b/frontend/cluesheet-web/src/app/page.module.css new file mode 100644 index 0000000..a11c8f3 --- /dev/null +++ b/frontend/cluesheet-web/src/app/page.module.css @@ -0,0 +1,168 @@ +.page { + --gray-rgb: 0, 0, 0; + --gray-alpha-200: rgba(var(--gray-rgb), 0.08); + --gray-alpha-100: rgba(var(--gray-rgb), 0.05); + + --button-primary-hover: #383838; + --button-secondary-hover: #f2f2f2; + + display: grid; + grid-template-rows: 20px 1fr 20px; + align-items: center; + justify-items: center; + min-height: 100svh; + padding: 80px; + gap: 64px; + font-family: var(--font-geist-sans); +} + +@media (prefers-color-scheme: dark) { + .page { + --gray-rgb: 255, 255, 255; + --gray-alpha-200: rgba(var(--gray-rgb), 0.145); + --gray-alpha-100: rgba(var(--gray-rgb), 0.06); + + --button-primary-hover: #ccc; + --button-secondary-hover: #1a1a1a; + } +} + +.main { + display: flex; + flex-direction: column; + gap: 32px; + grid-row-start: 2; +} + +.main ol { + font-family: var(--font-geist-mono); + padding-left: 0; + margin: 0; + font-size: 14px; + line-height: 24px; + letter-spacing: -0.01em; + list-style-position: inside; +} + +.main li:not(:last-of-type) { + margin-bottom: 8px; +} + +.main code { + font-family: inherit; + background: var(--gray-alpha-100); + padding: 2px 4px; + border-radius: 4px; + font-weight: 600; +} + +.ctas { + display: flex; + gap: 16px; +} + +.ctas a { + appearance: none; + border-radius: 128px; + height: 48px; + padding: 0 20px; + border: none; + border: 1px solid transparent; + transition: + background 0.2s, + color 0.2s, + border-color 0.2s; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 16px; + line-height: 20px; + font-weight: 500; +} + +a.primary { + background: var(--foreground); + color: var(--background); + gap: 8px; +} + +a.secondary { + border-color: var(--gray-alpha-200); + min-width: 158px; +} + +.footer { + grid-row-start: 3; + display: flex; + gap: 24px; +} + +.footer a { + display: flex; + align-items: center; + gap: 8px; +} + +.footer img { + flex-shrink: 0; +} + +/* Enable hover only on non-touch devices */ +@media (hover: hover) and (pointer: fine) { + a.primary:hover { + background: var(--button-primary-hover); + border-color: transparent; + } + + a.secondary:hover { + background: var(--button-secondary-hover); + border-color: transparent; + } + + .footer a:hover { + text-decoration: underline; + text-underline-offset: 4px; + } +} + +@media (max-width: 600px) { + .page { + padding: 32px; + padding-bottom: 80px; + } + + .main { + align-items: center; + } + + .main ol { + text-align: center; + } + + .ctas { + flex-direction: column; + } + + .ctas a { + font-size: 14px; + height: 40px; + padding: 0 16px; + } + + a.secondary { + min-width: auto; + } + + .footer { + flex-wrap: wrap; + align-items: center; + justify-content: center; + } +} + +@media (prefers-color-scheme: dark) { + .logo { + filter: invert(); + } +} diff --git a/frontend/cluesheet-web/src/app/page.tsx b/frontend/cluesheet-web/src/app/page.tsx new file mode 100644 index 0000000..84af2cb --- /dev/null +++ b/frontend/cluesheet-web/src/app/page.tsx @@ -0,0 +1,95 @@ +import Image from "next/image"; +import styles from "./page.module.css"; + +export default function Home() { + return ( +
+
+ Next.js logo +
    +
  1. + Get started by editing src/app/page.tsx. +
  2. +
  3. Save and see your changes instantly.
  4. +
+ + +
+ +
+ ); +} diff --git a/frontend/cluesheet-web/tsconfig.json b/frontend/cluesheet-web/tsconfig.json new file mode 100644 index 0000000..c133409 --- /dev/null +++ b/frontend/cluesheet-web/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} From 6b815b044206b50f1cfa076ae217dd1d97cb7f3d Mon Sep 17 00:00:00 2001 From: Will Nilges Date: Sun, 4 May 2025 00:47:03 -0400 Subject: [PATCH 02/12] steal stuff from infosys --- frontend/cluesheet-web/package-lock.json | 949 +++++++++++++++++- frontend/cluesheet-web/package.json | 12 +- frontend/cluesheet-web/src/app/globals.css | 41 - frontend/cluesheet-web/src/app/layout.tsx | 3 + frontend/cluesheet-web/src/app/page.tsx | 83 +- .../cluesheet-web/src/components/NavBar.tsx | 76 ++ .../cluesheet-web/src/components/Profile.tsx | 55 + .../cluesheet-web/src/lib/Configuration.ts | 24 + .../src/lib/SSODisabledDefaults.ts | 57 ++ frontend/cluesheet-web/src/lib/UserInfo.ts | 28 + 10 files changed, 1200 insertions(+), 128 deletions(-) create mode 100644 frontend/cluesheet-web/src/components/NavBar.tsx create mode 100644 frontend/cluesheet-web/src/components/Profile.tsx create mode 100644 frontend/cluesheet-web/src/lib/Configuration.ts create mode 100644 frontend/cluesheet-web/src/lib/SSODisabledDefaults.ts create mode 100644 frontend/cluesheet-web/src/lib/UserInfo.ts diff --git a/frontend/cluesheet-web/package-lock.json b/frontend/cluesheet-web/package-lock.json index 90e9ff8..4af1294 100644 --- a/frontend/cluesheet-web/package-lock.json +++ b/frontend/cluesheet-web/package-lock.json @@ -8,17 +8,62 @@ "name": "cluesheet-web", "version": "0.1.0", "dependencies": { + "@axa-fr/react-oidc": "^7.25.12", + "csh-material-bootstrap": "^4.6.2", "next": "15.3.1", + "powerhooks": "^2.0.1", "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-dom": "^19.0.0", + "react-router-dom": "^7.5.3", + "reactstrap": "^9.2.3" }, "devDependencies": { "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", + "sass": "^1.87.0", "typescript": "^5" } }, + "node_modules/@axa-fr/oidc-client": { + "version": "7.25.12", + "resolved": "https://registry.npmjs.org/@axa-fr/oidc-client/-/oidc-client-7.25.12.tgz", + "integrity": "sha512-Xxl6scLYFcw1/yhGjwZWy7yKOGc05P9B7QhdX9IGfT1GsfdteqpnXhbFW/IpDFrYV02mLaqjfISRWottA8pixw==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@axa-fr/oidc-client-service-worker": "7.25.12" + } + }, + "node_modules/@axa-fr/oidc-client-service-worker": { + "version": "7.25.12", + "resolved": "https://registry.npmjs.org/@axa-fr/oidc-client-service-worker/-/oidc-client-service-worker-7.25.12.tgz", + "integrity": "sha512-hUOCBt8pKGR+rDHY7WTc6IlgyxZFvlPcrbeR0F9b7EbHgKg3dT2Fc5Ai8FhgmNLvU/GY+VoKLzS85fsxNiw5vA==", + "license": "MIT" + }, + "node_modules/@axa-fr/react-oidc": { + "version": "7.25.12", + "resolved": "https://registry.npmjs.org/@axa-fr/react-oidc/-/react-oidc-7.25.12.tgz", + "integrity": "sha512-/zYLhHzuTp05QIIusyxCnYv5FqoVKbDTAxBDK6B20IzjRAIWF1REgrxTbQLupcXGUtnkkw48wSEmFdhIgR8tzg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@axa-fr/oidc-client": "7.25.12", + "@axa-fr/oidc-client-service-worker": "7.25.12" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz", + "integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@emnapi/runtime": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz", @@ -540,6 +585,340 @@ "node": ">= 10" } }, + "node_modules/@parcel/watcher": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", + "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.1", + "@parcel/watcher-darwin-arm64": "2.5.1", + "@parcel/watcher-darwin-x64": "2.5.1", + "@parcel/watcher-freebsd-x64": "2.5.1", + "@parcel/watcher-linux-arm-glibc": "2.5.1", + "@parcel/watcher-linux-arm-musl": "2.5.1", + "@parcel/watcher-linux-arm64-glibc": "2.5.1", + "@parcel/watcher-linux-arm64-musl": "2.5.1", + "@parcel/watcher-linux-x64-glibc": "2.5.1", + "@parcel/watcher-linux-x64-musl": "2.5.1", + "@parcel/watcher-win32-arm64": "2.5.1", + "@parcel/watcher-win32-ia32": "2.5.1", + "@parcel/watcher-win32-x64": "2.5.1" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", + "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", + "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", + "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", + "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", + "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", + "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", + "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", + "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", + "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", + "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", + "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", + "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", + "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher/node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, "node_modules/@swc/counter": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", @@ -585,6 +964,20 @@ "@types/react": "^19.0.0" } }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -616,6 +1009,28 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "license": "MIT" + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -667,13 +1082,43 @@ "simple-swizzle": "^0.2.2" } }, + "node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/csh-material-bootstrap": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/csh-material-bootstrap/-/csh-material-bootstrap-4.6.2.tgz", + "integrity": "sha512-5yEOZ4OLtRmfLH5SZqOzvOQqvSRO8oA39Tu8fs5yvyGzxQiMULgyvL4ZfpG1xj7i8qLTylN1w863yiD7KgfF5w==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true, "license": "MIT" }, + "node_modules/d": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz", + "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==", + "license": "ISC", + "dependencies": { + "es5-ext": "^0.10.64", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.12" + } + }, "node_modules/detect-libc": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", @@ -684,6 +1129,134 @@ "node": ">=8" } }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/es5-ext": { + "version": "0.10.64", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", + "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", + "hasInstallScript": true, + "license": "ISC", + "dependencies": { + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.3", + "esniff": "^2.0.1", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", + "license": "MIT", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/es6-symbol": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz", + "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==", + "license": "ISC", + "dependencies": { + "d": "^1.0.2", + "ext": "^1.7.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/es6-weak-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz", + "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==", + "license": "ISC", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.46", + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/esniff": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", + "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", + "license": "ISC", + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.62", + "event-emitter": "^0.3.5", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", + "license": "MIT", + "dependencies": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, + "node_modules/evt": { + "version": "2.5.9", + "resolved": "https://registry.npmjs.org/evt/-/evt-2.5.9.tgz", + "integrity": "sha512-GpjX476FSlttEGWHT8BdVMoI8wGXQGbEOtKcP4E+kggg+yJzXBZN2n4x7TS/zPBJ1DZqWI+rguZZApjjzQ0HpA==", + "license": "MIT", + "dependencies": { + "minimal-polyfills": "^2.2.3", + "run-exclusive": "^2.2.19", + "tsafe": "^1.8.5" + } + }, + "node_modules/ext": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", + "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", + "license": "ISC", + "dependencies": { + "type": "^2.7.2" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/immutable": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.1.tgz", + "integrity": "sha512-3jatXi9ObIsPGr3N5hGw/vWWcTkq6hUYhpQz4k0wLC+owqWi/LiugIw9x0EdNZ2yGedKN/HzePiBvaJRXa0Ujg==", + "devOptional": true, + "license": "MIT" + }, "node_modules/is-arrayish": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", @@ -691,6 +1264,115 @@ "license": "MIT", "optional": true }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", + "integrity": "sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ==", + "license": "MIT", + "dependencies": { + "es5-ext": "~0.10.2" + } + }, + "node_modules/memoizee": { + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.17.tgz", + "integrity": "sha512-DGqD7Hjpi/1or4F/aYAspXKNm5Yili0QDAFAY4QYvpqpgiY6+1jOfqpmByzjxbWd/T9mChbCArXAbDAsTm5oXA==", + "license": "ISC", + "dependencies": { + "d": "^1.0.2", + "es5-ext": "^0.10.64", + "es6-weak-map": "^2.0.3", + "event-emitter": "^0.3.5", + "is-promise": "^2.2.2", + "lru-queue": "^0.1.0", + "next-tick": "^1.1.0", + "timers-ext": "^0.1.7" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimal-polyfills": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/minimal-polyfills/-/minimal-polyfills-2.2.3.tgz", + "integrity": "sha512-oxdmJ9cL+xV72h0xYxp4tP2d5/fTBpP45H8DIOn9pASuF8a3IYTf+25fMGDYGiWW+MFsuog6KD6nfmhZJQ+uUw==", + "license": "MIT" + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -763,12 +1445,49 @@ } } }, + "node_modules/next-tick": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", + "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", + "license": "ISC" + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "license": "ISC" }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -797,6 +1516,28 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/powerhooks": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/powerhooks/-/powerhooks-2.0.1.tgz", + "integrity": "sha512-G4oStGCXGHBexDEAlRfrYRESgticY6oOOtkz9AEwmUt6uItZwNqoDELWyZiEoHjOrwuLDMd6+S0IHj9lBLz9dA==", + "license": "MIT", + "dependencies": { + "evt": "^2.5.9", + "memoizee": "^0.4.17", + "tsafe": "^1.8.5" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, "node_modules/react": { "version": "19.1.0", "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", @@ -818,6 +1559,150 @@ "react": "^19.1.0" } }, + "node_modules/react-fast-compare": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", + "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==", + "license": "MIT" + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/react-router": { + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.5.3.tgz", + "integrity": "sha512-3iUDM4/fZCQ89SXlDa+Ph3MevBrozBAI655OAfWQlTm9nBR0IKlrmNwFow5lPHttbwvITZfkeeeZFP6zt3F7pw==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0", + "turbo-stream": "2.4.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.5.3.tgz", + "integrity": "sha512-cK0jSaTyW4jV9SRKAItMIQfWZ/D6WEZafgHuuCb9g+SjhLolY78qc+De4w/Cz9ybjvLzShAmaIMEXt8iF1Cm+A==", + "license": "MIT", + "dependencies": { + "react-router": "7.5.3" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/reactstrap": { + "version": "9.2.3", + "resolved": "https://registry.npmjs.org/reactstrap/-/reactstrap-9.2.3.tgz", + "integrity": "sha512-1nXy7FIBIoOgXr3AIHOpgzcZXdj6rZE5YvNSPd1hYgwv8X64m6TAJsU0ExlieJdlRXhaRfTYRSZoTWa127b0gw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@popperjs/core": "^2.6.0", + "classnames": "^2.2.3", + "prop-types": "^15.5.8", + "react-popper": "^2.2.4", + "react-transition-group": "^4.4.2" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/reactstrap/node_modules/react-popper": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.3.0.tgz", + "integrity": "sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==", + "license": "MIT", + "dependencies": { + "react-fast-compare": "^3.0.1", + "warning": "^4.0.2" + }, + "peerDependencies": { + "@popperjs/core": "^2.0.0", + "react": "^16.8.0 || ^17 || ^18", + "react-dom": "^16.8.0 || ^17 || ^18" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/run-exclusive": { + "version": "2.2.19", + "resolved": "https://registry.npmjs.org/run-exclusive/-/run-exclusive-2.2.19.tgz", + "integrity": "sha512-K3mdoAi7tjJ/qT7Flj90L7QyPozwUaAG+CVhkdDje4HLKXUYC3N/Jzkau3flHVDLQVhiHBtcimVodMjN9egYbA==", + "license": "MIT", + "dependencies": { + "minimal-polyfills": "^2.2.3" + } + }, + "node_modules/sass": { + "version": "1.87.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.87.0.tgz", + "integrity": "sha512-d0NoFH4v6SjEK7BoX810Jsrhj7IQSYHAHLi/iSpgqKc7LaIDshFRlSg5LOymf9FqQhxEHs2W5ZQXlvy0KD45Uw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.0", + "immutable": "^5.0.2", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, "node_modules/scheduler": { "version": "0.26.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", @@ -837,6 +1722,12 @@ "node": ">=10" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "license": "MIT" + }, "node_modules/sharp": { "version": "0.34.1", "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.1.tgz", @@ -928,12 +1819,57 @@ } } }, + "node_modules/timers-ext": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.8.tgz", + "integrity": "sha512-wFH7+SEAcKfJpfLPkrgMPvvwnEtj8W4IurvEyrKsDleXnKLCDw71w8jltvfLa8Rm4qQxxT4jmDBYbJG/z7qoww==", + "license": "ISC", + "dependencies": { + "es5-ext": "^0.10.64", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tsafe": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/tsafe/-/tsafe-1.8.5.tgz", + "integrity": "sha512-LFWTWQrW6rwSY+IBNFl2ridGfUzVsPwrZ26T4KUJww/py8rzaQ/SY+MIz6YROozpUCaRcuISqagmlwub9YT9kw==", + "license": "MIT" + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/turbo-stream": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz", + "integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==", + "license": "ISC" + }, + "node_modules/type": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", + "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==", + "license": "ISC" + }, "node_modules/typescript": { "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", @@ -954,6 +1890,15 @@ "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", "dev": true, "license": "MIT" + }, + "node_modules/warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } } } } diff --git a/frontend/cluesheet-web/package.json b/frontend/cluesheet-web/package.json index 4ec6352..74b1475 100644 --- a/frontend/cluesheet-web/package.json +++ b/frontend/cluesheet-web/package.json @@ -9,14 +9,20 @@ "lint": "next lint" }, "dependencies": { + "@axa-fr/react-oidc": "^7.25.12", + "csh-material-bootstrap": "^4.6.2", + "next": "15.3.1", + "powerhooks": "^2.0.1", "react": "^19.0.0", "react-dom": "^19.0.0", - "next": "15.3.1" + "react-router-dom": "^7.5.3", + "reactstrap": "^9.2.3" }, "devDependencies": { - "typescript": "^5", "@types/node": "^20", "@types/react": "^19", - "@types/react-dom": "^19" + "@types/react-dom": "^19", + "sass": "^1.87.0", + "typescript": "^5" } } diff --git a/frontend/cluesheet-web/src/app/globals.css b/frontend/cluesheet-web/src/app/globals.css index e3734be..8b13789 100644 --- a/frontend/cluesheet-web/src/app/globals.css +++ b/frontend/cluesheet-web/src/app/globals.css @@ -1,42 +1 @@ -:root { - --background: #ffffff; - --foreground: #171717; -} -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; - } -} - -html, -body { - max-width: 100vw; - overflow-x: hidden; -} - -body { - color: var(--foreground); - background: var(--background); - font-family: Arial, Helvetica, sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -* { - box-sizing: border-box; - padding: 0; - margin: 0; -} - -a { - color: inherit; - text-decoration: none; -} - -@media (prefers-color-scheme: dark) { - html { - color-scheme: dark; - } -} diff --git a/frontend/cluesheet-web/src/app/layout.tsx b/frontend/cluesheet-web/src/app/layout.tsx index 42fc323..18643a3 100644 --- a/frontend/cluesheet-web/src/app/layout.tsx +++ b/frontend/cluesheet-web/src/app/layout.tsx @@ -1,6 +1,8 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; +import 'csh-material-bootstrap/dist/csh-material-bootstrap.css'; +import NavBar from "@/components/NavBar"; const geistSans = Geist({ variable: "--font-geist-sans", @@ -25,6 +27,7 @@ export default function RootLayout({ return ( + {children} diff --git a/frontend/cluesheet-web/src/app/page.tsx b/frontend/cluesheet-web/src/app/page.tsx index 84af2cb..7700efe 100644 --- a/frontend/cluesheet-web/src/app/page.tsx +++ b/frontend/cluesheet-web/src/app/page.tsx @@ -5,90 +5,9 @@ export default function Home() { return (
- Next.js logo -
    -
  1. - Get started by editing src/app/page.tsx. -
  2. -
  3. Save and see your changes instantly.
  4. -
- - +

Welcome to willard's world

); diff --git a/frontend/cluesheet-web/src/components/NavBar.tsx b/frontend/cluesheet-web/src/components/NavBar.tsx new file mode 100644 index 0000000..a2dedb7 --- /dev/null +++ b/frontend/cluesheet-web/src/components/NavBar.tsx @@ -0,0 +1,76 @@ +"use client" +import React from "react"; +import { + Collapse, + Container, + Nav, + Navbar, + NavbarToggler, + NavItem, +} from "reactstrap"; +import {NavLink} from "react-router-dom"; +import Profile from "./Profile"; + +import UserInfo from "@/lib/UserInfo"; +import {SSOEnabled} from "@/lib/Configuration"; +import { + getUseOidcAccessToken, + NoSSOUserInfo, +} from "@/lib/SSODisabledDefaults"; + +const NavBar: React.FunctionComponent = () => { + const [isOpen, setIsOpen] = React.useState(false); + + const toggle = () => { + setIsOpen(!isOpen); + }; + const {accessTokenPayload} = getUseOidcAccessToken()(); + const userInfo = SSOEnabled + ? (accessTokenPayload as UserInfo) + : NoSSOUserInfo; + + return ( +
+ + + + InfoSys + + + + + + + + +
+ ); +}; + +export default NavBar; diff --git a/frontend/cluesheet-web/src/components/Profile.tsx b/frontend/cluesheet-web/src/components/Profile.tsx new file mode 100644 index 0000000..7f5acb9 --- /dev/null +++ b/frontend/cluesheet-web/src/components/Profile.tsx @@ -0,0 +1,55 @@ +import { + DropdownItem, + DropdownMenu, + DropdownToggle, + UncontrolledDropdown, +} from "reactstrap"; + +import React from "react"; +import UserInfo from "@/lib/UserInfo"; +import {SSOEnabled} from "@/lib/Configuration"; +import { + getUseOidcAccessToken, + getUseOidcHook, + NoSSOProfilePicture, + NoSSOUserInfo, +} from "@/lib/SSODisabledDefaults"; + +const Profile: React.FunctionComponent = () => { + const {logout} = getUseOidcHook()(); + const {accessTokenPayload} = getUseOidcAccessToken()(); + const userInfo = SSOEnabled + ? (accessTokenPayload as UserInfo) + : NoSSOUserInfo; + + return ( + + + + ({userInfo.preferred_username}) + + + + { + // to add stuff to the profile dropdown, you can + } + Members + + logout(null)}>Logout + + + ); +}; + +export default Profile; diff --git a/frontend/cluesheet-web/src/lib/Configuration.ts b/frontend/cluesheet-web/src/lib/Configuration.ts new file mode 100644 index 0000000..2de5013 --- /dev/null +++ b/frontend/cluesheet-web/src/lib/Configuration.ts @@ -0,0 +1,24 @@ +import {OidcConfiguration} from "@axa-fr/react-oidc"; + +const oidcConfig: OidcConfiguration = { + client_id: process.env.NEXT_PUBLIC_SSO_CLIENT_ID ?? "test", + redirect_uri: `${window.location.protocol}//${window.location.hostname}${ + window.location.port ? `:${window.location.port}` : "" + }/authentication/callback`, + scope: "openid profile email offline_access", + authority: process.env.NEXT_PUBLIC_SSO_AUTHORITY ?? "", + silent_redirect_uri: `${window.location.protocol}//${ + window.location.hostname + }${ + window.location.port ? `:${window.location.port}` : "" + }/authentication/silent_callback`, + service_worker_only: false, + refresh_time_before_tokens_expiration_in_second: 10, +}; + +export const apiPrefix = process.env.NEXT_PUBLIC_API_PREFIX; + +export const SSOEnabled = false; + // (import.meta.env.VITE_SSO_ENABLED ?? "true") === "true"; + +export default oidcConfig; diff --git a/frontend/cluesheet-web/src/lib/SSODisabledDefaults.ts b/frontend/cluesheet-web/src/lib/SSODisabledDefaults.ts new file mode 100644 index 0000000..a8e6e9b --- /dev/null +++ b/frontend/cluesheet-web/src/lib/SSODisabledDefaults.ts @@ -0,0 +1,57 @@ +// if your app requires SSO you can delete this page +import UserInfo from "./UserInfo"; +import {useOidc, useOidcAccessToken} from "@axa-fr/react-oidc"; +import { SSOEnabled } from "./Configuration"; + +export const useOidcNoSSO = () => { + return { + login: () => alert("SSO Disabled, you can change this in configuration.ts"), + logout: () => + alert("SSO Disabled, you can change this in configuration.ts"), + isAuthenticated: false, + }; +}; + +export const useOidcAccessTokenNoSSO = () => { + return {accessTokenPayload: ""}; +}; + +export const NoSSOUserInfo: UserInfo = { + exp: 0, + auth_time: 0, + jti: "", + iss: "", + aud: "", + sub: "", + typ: "", + azp: "", + nonce: "", + session_state: "", + acr: "", + allowed_origins: [], + realm_access: { + roles: [], + }, + resource_access: {}, + scope: "", + sid: "", + email_verified: false, + name: "Foo Bar", + groups: [], + preferred_username: "foolish", + given_name: "Foo", + family_name: "Bar", + email: "baz@csh.rit.edu", + uuid: "", +}; + +export const NoSSOProfilePicture = + "https://cdn.discordapp.com/attachments/719366780606480384/1035722437049982986/csh.png"; + +export const getUseOidcHook = () => { + return SSOEnabled ? useOidc : useOidcNoSSO; +}; + +export const getUseOidcAccessToken = () => { + return SSOEnabled ? useOidcAccessToken : useOidcAccessTokenNoSSO; +}; diff --git a/frontend/cluesheet-web/src/lib/UserInfo.ts b/frontend/cluesheet-web/src/lib/UserInfo.ts new file mode 100644 index 0000000..30ce5dc --- /dev/null +++ b/frontend/cluesheet-web/src/lib/UserInfo.ts @@ -0,0 +1,28 @@ +interface UserInfo { + exp: number; + auth_time: number; + jti: string; + iss: string; + aud: string; + sub: string; + typ: string; + azp: string; + nonce: string; + session_state: string; + acr: string; + allowed_origins: string[]; + realm_access: {roles: string[]}; + resource_access: {[key: string]: {roles: string[]}}; + scope: string; + sid: string; + email_verified: boolean; + name: string; + groups: string[]; + preferred_username: string; + given_name: string; + family_name: string; + email: string; + uuid: string; +} + +export default UserInfo; From 11653a9809a66f856def009bec387e3714996fb3 Mon Sep 17 00:00:00 2001 From: Will Nilges Date: Sun, 4 May 2025 02:03:45 -0400 Subject: [PATCH 03/12] checkpoint --- .../.github/workflows/playwright.yml | 27 + frontend/cluesheet-web/.gitignore | 7 + frontend/cluesheet-web/e2e/example.spec.ts | 18 + frontend/cluesheet-web/package-lock.json | 81 +++ frontend/cluesheet-web/package.json | 2 + frontend/cluesheet-web/playwright.config.ts | 79 +++ .../src/app/cluesheet/[cluesheet_id]/page.tsx | 18 + frontend/cluesheet-web/src/app/globals.css | 1 - frontend/cluesheet-web/src/app/layout.tsx | 4 +- frontend/cluesheet-web/src/app/page.tsx | 3 +- .../cluesheet-web/src/components/NavBar.tsx | 127 ++-- .../cluesheet-web/src/components/Profile.tsx | 6 +- .../src/components/viewer/ClueGroup.tsx | 0 .../src/components/viewer/ClueList.tsx | 3 + .../src/components/viewer/LineItem.tsx | 0 .../cluesheet-web/src/lib/Configuration.ts | 6 +- .../src/lib/SSODisabledDefaults.ts | 4 +- frontend/cluesheet-web/src/lib/UserInfo.ts | 4 +- .../tests-examples/demo-todo-app.spec.ts | 437 ++++++++++++ .../tests/00_join_form_meshdb_500.spec.ts | 74 ++ .../tests/01_join_form_meshdb_down.spec.ts | 77 +++ .../cluesheet-web/tests/02_join_form.spec.ts | 630 ++++++++++++++++++ .../tests/03_nn_assign_form.spec.ts | 79 +++ .../tests/04_join_form_s3_down.spec.ts | 30 + .../tests/05_join_form_i18n.spec.ts | 45 ++ frontend/cluesheet-web/tests/mock/handlers.ts | 187 ++++++ frontend/cluesheet-web/tests/mock/test.ts | 16 + .../tests/mock_meshdb_500/handlers.ts | 21 + .../tests/mock_meshdb_500/test.ts | 16 + frontend/cluesheet-web/tests/util.ts | 185 +++++ 30 files changed, 2127 insertions(+), 60 deletions(-) create mode 100644 frontend/cluesheet-web/.github/workflows/playwright.yml create mode 100644 frontend/cluesheet-web/e2e/example.spec.ts create mode 100644 frontend/cluesheet-web/playwright.config.ts create mode 100644 frontend/cluesheet-web/src/app/cluesheet/[cluesheet_id]/page.tsx create mode 100644 frontend/cluesheet-web/src/components/viewer/ClueGroup.tsx create mode 100644 frontend/cluesheet-web/src/components/viewer/ClueList.tsx create mode 100644 frontend/cluesheet-web/src/components/viewer/LineItem.tsx create mode 100644 frontend/cluesheet-web/tests-examples/demo-todo-app.spec.ts create mode 100644 frontend/cluesheet-web/tests/00_join_form_meshdb_500.spec.ts create mode 100644 frontend/cluesheet-web/tests/01_join_form_meshdb_down.spec.ts create mode 100644 frontend/cluesheet-web/tests/02_join_form.spec.ts create mode 100644 frontend/cluesheet-web/tests/03_nn_assign_form.spec.ts create mode 100644 frontend/cluesheet-web/tests/04_join_form_s3_down.spec.ts create mode 100644 frontend/cluesheet-web/tests/05_join_form_i18n.spec.ts create mode 100644 frontend/cluesheet-web/tests/mock/handlers.ts create mode 100644 frontend/cluesheet-web/tests/mock/test.ts create mode 100644 frontend/cluesheet-web/tests/mock_meshdb_500/handlers.ts create mode 100644 frontend/cluesheet-web/tests/mock_meshdb_500/test.ts create mode 100644 frontend/cluesheet-web/tests/util.ts diff --git a/frontend/cluesheet-web/.github/workflows/playwright.yml b/frontend/cluesheet-web/.github/workflows/playwright.yml new file mode 100644 index 0000000..3eb1314 --- /dev/null +++ b/frontend/cluesheet-web/.github/workflows/playwright.yml @@ -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 ci + - name: Install Playwright Browsers + run: npx playwright install --with-deps + - name: Run Playwright tests + run: npx playwright test + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/frontend/cluesheet-web/.gitignore b/frontend/cluesheet-web/.gitignore index 5ef6a52..8972f81 100644 --- a/frontend/cluesheet-web/.gitignore +++ b/frontend/cluesheet-web/.gitignore @@ -39,3 +39,10 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +# Playwright +node_modules/ +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/frontend/cluesheet-web/e2e/example.spec.ts b/frontend/cluesheet-web/e2e/example.spec.ts new file mode 100644 index 0000000..54a906a --- /dev/null +++ b/frontend/cluesheet-web/e2e/example.spec.ts @@ -0,0 +1,18 @@ +import { test, expect } from '@playwright/test'; + +test('has title', async ({ page }) => { + await page.goto('https://playwright.dev/'); + + // Expect a title "to contain" a substring. + await expect(page).toHaveTitle(/Playwright/); +}); + +test('get started link', async ({ page }) => { + await page.goto('https://playwright.dev/'); + + // Click the get started link. + await page.getByRole('link', { name: 'Get started' }).click(); + + // Expects page to have a heading with the name of Installation. + await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible(); +}); diff --git a/frontend/cluesheet-web/package-lock.json b/frontend/cluesheet-web/package-lock.json index 4af1294..af2362e 100644 --- a/frontend/cluesheet-web/package-lock.json +++ b/frontend/cluesheet-web/package-lock.json @@ -18,9 +18,11 @@ "reactstrap": "^9.2.3" }, "devDependencies": { + "@playwright/test": "^1.52.0", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", + "prettier": "3.5.3", "sass": "^1.87.0", "typescript": "^5" } @@ -909,6 +911,22 @@ "node": ">=0.10" } }, + "node_modules/@playwright/test": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.52.0.tgz", + "integrity": "sha512-uh6W7sb55hl7D6vsAeA+V2p5JnlAqzhqFyF0VcJkKZXkgnFcVG9PziERRHQfPLfNGx1C292a4JqbWzhR8L4R1g==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.52.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@popperjs/core": { "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", @@ -1250,6 +1268,21 @@ "node": ">=8" } }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/immutable": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.1.tgz", @@ -1488,6 +1521,38 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/playwright": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.52.0.tgz", + "integrity": "sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.52.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.52.0.tgz", + "integrity": "sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -1527,6 +1592,22 @@ "tsafe": "^1.8.5" } }, + "node_modules/prettier": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", + "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", diff --git a/frontend/cluesheet-web/package.json b/frontend/cluesheet-web/package.json index 74b1475..f1f4638 100644 --- a/frontend/cluesheet-web/package.json +++ b/frontend/cluesheet-web/package.json @@ -19,9 +19,11 @@ "reactstrap": "^9.2.3" }, "devDependencies": { + "@playwright/test": "^1.52.0", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", + "prettier": "3.5.3", "sass": "^1.87.0", "typescript": "^5" } diff --git a/frontend/cluesheet-web/playwright.config.ts b/frontend/cluesheet-web/playwright.config.ts new file mode 100644 index 0000000..efd9b88 --- /dev/null +++ b/frontend/cluesheet-web/playwright.config.ts @@ -0,0 +1,79 @@ +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: './e2e', + /* 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://127.0.0.1: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://127.0.0.1:3000', + // reuseExistingServer: !process.env.CI, + // }, +}); diff --git a/frontend/cluesheet-web/src/app/cluesheet/[cluesheet_id]/page.tsx b/frontend/cluesheet-web/src/app/cluesheet/[cluesheet_id]/page.tsx new file mode 100644 index 0000000..ea16e23 --- /dev/null +++ b/frontend/cluesheet-web/src/app/cluesheet/[cluesheet_id]/page.tsx @@ -0,0 +1,18 @@ +export const metadata = { + title: "View cluesheet", + description: "View an mf cluesheet kerchoo", +}; + +export default async function CluesheetViewer({ + params, +}: { + params: Promise<{ cluesheet_id: string }>; +}) { + const { cluesheet_id } = await params; + return ( + <> +
+
+ + ); +} diff --git a/frontend/cluesheet-web/src/app/globals.css b/frontend/cluesheet-web/src/app/globals.css index 8b13789..e69de29 100644 --- a/frontend/cluesheet-web/src/app/globals.css +++ b/frontend/cluesheet-web/src/app/globals.css @@ -1 +0,0 @@ - diff --git a/frontend/cluesheet-web/src/app/layout.tsx b/frontend/cluesheet-web/src/app/layout.tsx index 18643a3..1edb379 100644 --- a/frontend/cluesheet-web/src/app/layout.tsx +++ b/frontend/cluesheet-web/src/app/layout.tsx @@ -1,7 +1,7 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; -import 'csh-material-bootstrap/dist/csh-material-bootstrap.css'; +import "csh-material-bootstrap/dist/csh-material-bootstrap.css"; import NavBar from "@/components/NavBar"; const geistSans = Geist({ @@ -27,7 +27,7 @@ export default function RootLayout({ return ( - + {children} diff --git a/frontend/cluesheet-web/src/app/page.tsx b/frontend/cluesheet-web/src/app/page.tsx index 7700efe..ac28cdf 100644 --- a/frontend/cluesheet-web/src/app/page.tsx +++ b/frontend/cluesheet-web/src/app/page.tsx @@ -7,8 +7,7 @@ export default function Home() {

Welcome to willard's world

-
-
+
); } diff --git a/frontend/cluesheet-web/src/components/NavBar.tsx b/frontend/cluesheet-web/src/components/NavBar.tsx index a2dedb7..d2014a0 100644 --- a/frontend/cluesheet-web/src/components/NavBar.tsx +++ b/frontend/cluesheet-web/src/components/NavBar.tsx @@ -1,4 +1,4 @@ -"use client" +"use client"; import React from "react"; import { Collapse, @@ -8,11 +8,11 @@ import { NavbarToggler, NavItem, } from "reactstrap"; -import {NavLink} from "react-router-dom"; +import { NavLink } from "react-router-dom"; import Profile from "./Profile"; import UserInfo from "@/lib/UserInfo"; -import {SSOEnabled} from "@/lib/Configuration"; +import { SSOEnabled } from "@/lib/Configuration"; import { getUseOidcAccessToken, NoSSOUserInfo, @@ -24,52 +24,89 @@ const NavBar: React.FunctionComponent = () => { const toggle = () => { setIsOpen(!isOpen); }; - const {accessTokenPayload} = getUseOidcAccessToken()(); + const { accessTokenPayload } = getUseOidcAccessToken()(); const userInfo = SSOEnabled ? (accessTokenPayload as UserInfo) : NoSSOUserInfo; - + return ( -
- - - - InfoSys - - - - - - - - -
+ ); }; diff --git a/frontend/cluesheet-web/src/components/Profile.tsx b/frontend/cluesheet-web/src/components/Profile.tsx index 7f5acb9..1195c34 100644 --- a/frontend/cluesheet-web/src/components/Profile.tsx +++ b/frontend/cluesheet-web/src/components/Profile.tsx @@ -7,7 +7,7 @@ import { import React from "react"; import UserInfo from "@/lib/UserInfo"; -import {SSOEnabled} from "@/lib/Configuration"; +import { SSOEnabled } from "@/lib/Configuration"; import { getUseOidcAccessToken, getUseOidcHook, @@ -16,8 +16,8 @@ import { } from "@/lib/SSODisabledDefaults"; const Profile: React.FunctionComponent = () => { - const {logout} = getUseOidcHook()(); - const {accessTokenPayload} = getUseOidcAccessToken()(); + const { logout } = getUseOidcHook()(); + const { accessTokenPayload } = getUseOidcAccessToken()(); const userInfo = SSOEnabled ? (accessTokenPayload as UserInfo) : NoSSOUserInfo; diff --git a/frontend/cluesheet-web/src/components/viewer/ClueGroup.tsx b/frontend/cluesheet-web/src/components/viewer/ClueGroup.tsx new file mode 100644 index 0000000..e69de29 diff --git a/frontend/cluesheet-web/src/components/viewer/ClueList.tsx b/frontend/cluesheet-web/src/components/viewer/ClueList.tsx new file mode 100644 index 0000000..5f5be16 --- /dev/null +++ b/frontend/cluesheet-web/src/components/viewer/ClueList.tsx @@ -0,0 +1,3 @@ +export default function ClueList() { + +} diff --git a/frontend/cluesheet-web/src/components/viewer/LineItem.tsx b/frontend/cluesheet-web/src/components/viewer/LineItem.tsx new file mode 100644 index 0000000..e69de29 diff --git a/frontend/cluesheet-web/src/lib/Configuration.ts b/frontend/cluesheet-web/src/lib/Configuration.ts index 2de5013..b2c5f12 100644 --- a/frontend/cluesheet-web/src/lib/Configuration.ts +++ b/frontend/cluesheet-web/src/lib/Configuration.ts @@ -1,4 +1,4 @@ -import {OidcConfiguration} from "@axa-fr/react-oidc"; +import { OidcConfiguration } from "@axa-fr/react-oidc"; const oidcConfig: OidcConfiguration = { client_id: process.env.NEXT_PUBLIC_SSO_CLIENT_ID ?? "test", @@ -16,9 +16,9 @@ const oidcConfig: OidcConfiguration = { refresh_time_before_tokens_expiration_in_second: 10, }; -export const apiPrefix = process.env.NEXT_PUBLIC_API_PREFIX; +export const apiPrefix = process.env.NEXT_PUBLIC_API_PREFIX; export const SSOEnabled = false; - // (import.meta.env.VITE_SSO_ENABLED ?? "true") === "true"; +// (import.meta.env.VITE_SSO_ENABLED ?? "true") === "true"; export default oidcConfig; diff --git a/frontend/cluesheet-web/src/lib/SSODisabledDefaults.ts b/frontend/cluesheet-web/src/lib/SSODisabledDefaults.ts index a8e6e9b..81d9c55 100644 --- a/frontend/cluesheet-web/src/lib/SSODisabledDefaults.ts +++ b/frontend/cluesheet-web/src/lib/SSODisabledDefaults.ts @@ -1,6 +1,6 @@ // if your app requires SSO you can delete this page import UserInfo from "./UserInfo"; -import {useOidc, useOidcAccessToken} from "@axa-fr/react-oidc"; +import { useOidc, useOidcAccessToken } from "@axa-fr/react-oidc"; import { SSOEnabled } from "./Configuration"; export const useOidcNoSSO = () => { @@ -13,7 +13,7 @@ export const useOidcNoSSO = () => { }; export const useOidcAccessTokenNoSSO = () => { - return {accessTokenPayload: ""}; + return { accessTokenPayload: "" }; }; export const NoSSOUserInfo: UserInfo = { diff --git a/frontend/cluesheet-web/src/lib/UserInfo.ts b/frontend/cluesheet-web/src/lib/UserInfo.ts index 30ce5dc..f228ebf 100644 --- a/frontend/cluesheet-web/src/lib/UserInfo.ts +++ b/frontend/cluesheet-web/src/lib/UserInfo.ts @@ -11,8 +11,8 @@ interface UserInfo { session_state: string; acr: string; allowed_origins: string[]; - realm_access: {roles: string[]}; - resource_access: {[key: string]: {roles: string[]}}; + realm_access: { roles: string[] }; + resource_access: { [key: string]: { roles: string[] } }; scope: string; sid: string; email_verified: boolean; diff --git a/frontend/cluesheet-web/tests-examples/demo-todo-app.spec.ts b/frontend/cluesheet-web/tests-examples/demo-todo-app.spec.ts new file mode 100644 index 0000000..8641cb5 --- /dev/null +++ b/frontend/cluesheet-web/tests-examples/demo-todo-app.spec.ts @@ -0,0 +1,437 @@ +import { test, expect, type Page } from '@playwright/test'; + +test.beforeEach(async ({ page }) => { + await page.goto('https://demo.playwright.dev/todomvc'); +}); + +const TODO_ITEMS = [ + 'buy some cheese', + 'feed the cat', + 'book a doctors appointment' +] as const; + +test.describe('New Todo', () => { + test('should allow me to add todo items', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + // Create 1st todo. + await newTodo.fill(TODO_ITEMS[0]); + await newTodo.press('Enter'); + + // Make sure the list only has one todo item. + await expect(page.getByTestId('todo-title')).toHaveText([ + TODO_ITEMS[0] + ]); + + // Create 2nd todo. + await newTodo.fill(TODO_ITEMS[1]); + await newTodo.press('Enter'); + + // Make sure the list now has two todo items. + await expect(page.getByTestId('todo-title')).toHaveText([ + TODO_ITEMS[0], + TODO_ITEMS[1] + ]); + + await checkNumberOfTodosInLocalStorage(page, 2); + }); + + test('should clear text input field when an item is added', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + // Create one todo item. + await newTodo.fill(TODO_ITEMS[0]); + await newTodo.press('Enter'); + + // Check that input is empty. + await expect(newTodo).toBeEmpty(); + await checkNumberOfTodosInLocalStorage(page, 1); + }); + + test('should append new items to the bottom of the list', async ({ page }) => { + // Create 3 items. + await createDefaultTodos(page); + + // create a todo count locator + const todoCount = page.getByTestId('todo-count') + + // Check test using different methods. + await expect(page.getByText('3 items left')).toBeVisible(); + await expect(todoCount).toHaveText('3 items left'); + await expect(todoCount).toContainText('3'); + await expect(todoCount).toHaveText(/3/); + + // Check all items in one call. + await expect(page.getByTestId('todo-title')).toHaveText(TODO_ITEMS); + await checkNumberOfTodosInLocalStorage(page, 3); + }); +}); + +test.describe('Mark all as completed', () => { + test.beforeEach(async ({ page }) => { + await createDefaultTodos(page); + await checkNumberOfTodosInLocalStorage(page, 3); + }); + + test.afterEach(async ({ page }) => { + await checkNumberOfTodosInLocalStorage(page, 3); + }); + + test('should allow me to mark all items as completed', async ({ page }) => { + // Complete all todos. + await page.getByLabel('Mark all as complete').check(); + + // Ensure all todos have 'completed' class. + await expect(page.getByTestId('todo-item')).toHaveClass(['completed', 'completed', 'completed']); + await checkNumberOfCompletedTodosInLocalStorage(page, 3); + }); + + test('should allow me to clear the complete state of all items', async ({ page }) => { + const toggleAll = page.getByLabel('Mark all as complete'); + // Check and then immediately uncheck. + await toggleAll.check(); + await toggleAll.uncheck(); + + // Should be no completed classes. + await expect(page.getByTestId('todo-item')).toHaveClass(['', '', '']); + }); + + test('complete all checkbox should update state when items are completed / cleared', async ({ page }) => { + const toggleAll = page.getByLabel('Mark all as complete'); + await toggleAll.check(); + await expect(toggleAll).toBeChecked(); + await checkNumberOfCompletedTodosInLocalStorage(page, 3); + + // Uncheck first todo. + const firstTodo = page.getByTestId('todo-item').nth(0); + await firstTodo.getByRole('checkbox').uncheck(); + + // Reuse toggleAll locator and make sure its not checked. + await expect(toggleAll).not.toBeChecked(); + + await firstTodo.getByRole('checkbox').check(); + await checkNumberOfCompletedTodosInLocalStorage(page, 3); + + // Assert the toggle all is checked again. + await expect(toggleAll).toBeChecked(); + }); +}); + +test.describe('Item', () => { + + test('should allow me to mark items as complete', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + // Create two items. + for (const item of TODO_ITEMS.slice(0, 2)) { + await newTodo.fill(item); + await newTodo.press('Enter'); + } + + // Check first item. + const firstTodo = page.getByTestId('todo-item').nth(0); + await firstTodo.getByRole('checkbox').check(); + await expect(firstTodo).toHaveClass('completed'); + + // Check second item. + const secondTodo = page.getByTestId('todo-item').nth(1); + await expect(secondTodo).not.toHaveClass('completed'); + await secondTodo.getByRole('checkbox').check(); + + // Assert completed class. + await expect(firstTodo).toHaveClass('completed'); + await expect(secondTodo).toHaveClass('completed'); + }); + + test('should allow me to un-mark items as complete', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + // Create two items. + for (const item of TODO_ITEMS.slice(0, 2)) { + await newTodo.fill(item); + await newTodo.press('Enter'); + } + + const firstTodo = page.getByTestId('todo-item').nth(0); + const secondTodo = page.getByTestId('todo-item').nth(1); + const firstTodoCheckbox = firstTodo.getByRole('checkbox'); + + await firstTodoCheckbox.check(); + await expect(firstTodo).toHaveClass('completed'); + await expect(secondTodo).not.toHaveClass('completed'); + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + + await firstTodoCheckbox.uncheck(); + await expect(firstTodo).not.toHaveClass('completed'); + await expect(secondTodo).not.toHaveClass('completed'); + await checkNumberOfCompletedTodosInLocalStorage(page, 0); + }); + + test('should allow me to edit an item', async ({ page }) => { + await createDefaultTodos(page); + + const todoItems = page.getByTestId('todo-item'); + const secondTodo = todoItems.nth(1); + await secondTodo.dblclick(); + await expect(secondTodo.getByRole('textbox', { name: 'Edit' })).toHaveValue(TODO_ITEMS[1]); + await secondTodo.getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); + await secondTodo.getByRole('textbox', { name: 'Edit' }).press('Enter'); + + // Explicitly assert the new text value. + await expect(todoItems).toHaveText([ + TODO_ITEMS[0], + 'buy some sausages', + TODO_ITEMS[2] + ]); + await checkTodosInLocalStorage(page, 'buy some sausages'); + }); +}); + +test.describe('Editing', () => { + test.beforeEach(async ({ page }) => { + await createDefaultTodos(page); + await checkNumberOfTodosInLocalStorage(page, 3); + }); + + test('should hide other controls when editing', async ({ page }) => { + const todoItem = page.getByTestId('todo-item').nth(1); + await todoItem.dblclick(); + await expect(todoItem.getByRole('checkbox')).not.toBeVisible(); + await expect(todoItem.locator('label', { + hasText: TODO_ITEMS[1], + })).not.toBeVisible(); + await checkNumberOfTodosInLocalStorage(page, 3); + }); + + test('should save edits on blur', async ({ page }) => { + const todoItems = page.getByTestId('todo-item'); + await todoItems.nth(1).dblclick(); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).dispatchEvent('blur'); + + await expect(todoItems).toHaveText([ + TODO_ITEMS[0], + 'buy some sausages', + TODO_ITEMS[2], + ]); + await checkTodosInLocalStorage(page, 'buy some sausages'); + }); + + test('should trim entered text', async ({ page }) => { + const todoItems = page.getByTestId('todo-item'); + await todoItems.nth(1).dblclick(); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(' buy some sausages '); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter'); + + await expect(todoItems).toHaveText([ + TODO_ITEMS[0], + 'buy some sausages', + TODO_ITEMS[2], + ]); + await checkTodosInLocalStorage(page, 'buy some sausages'); + }); + + test('should remove the item if an empty text string was entered', async ({ page }) => { + const todoItems = page.getByTestId('todo-item'); + await todoItems.nth(1).dblclick(); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(''); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter'); + + await expect(todoItems).toHaveText([ + TODO_ITEMS[0], + TODO_ITEMS[2], + ]); + }); + + test('should cancel edits on escape', async ({ page }) => { + const todoItems = page.getByTestId('todo-item'); + await todoItems.nth(1).dblclick(); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Escape'); + await expect(todoItems).toHaveText(TODO_ITEMS); + }); +}); + +test.describe('Counter', () => { + test('should display the current number of todo items', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + // create a todo count locator + const todoCount = page.getByTestId('todo-count') + + await newTodo.fill(TODO_ITEMS[0]); + await newTodo.press('Enter'); + + await expect(todoCount).toContainText('1'); + + await newTodo.fill(TODO_ITEMS[1]); + await newTodo.press('Enter'); + await expect(todoCount).toContainText('2'); + + await checkNumberOfTodosInLocalStorage(page, 2); + }); +}); + +test.describe('Clear completed button', () => { + test.beforeEach(async ({ page }) => { + await createDefaultTodos(page); + }); + + test('should display the correct text', async ({ page }) => { + await page.locator('.todo-list li .toggle').first().check(); + await expect(page.getByRole('button', { name: 'Clear completed' })).toBeVisible(); + }); + + test('should remove completed items when clicked', async ({ page }) => { + const todoItems = page.getByTestId('todo-item'); + await todoItems.nth(1).getByRole('checkbox').check(); + await page.getByRole('button', { name: 'Clear completed' }).click(); + await expect(todoItems).toHaveCount(2); + await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); + }); + + test('should be hidden when there are no items that are completed', async ({ page }) => { + await page.locator('.todo-list li .toggle').first().check(); + await page.getByRole('button', { name: 'Clear completed' }).click(); + await expect(page.getByRole('button', { name: 'Clear completed' })).toBeHidden(); + }); +}); + +test.describe('Persistence', () => { + test('should persist its data', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + for (const item of TODO_ITEMS.slice(0, 2)) { + await newTodo.fill(item); + await newTodo.press('Enter'); + } + + const todoItems = page.getByTestId('todo-item'); + const firstTodoCheck = todoItems.nth(0).getByRole('checkbox'); + await firstTodoCheck.check(); + await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); + await expect(firstTodoCheck).toBeChecked(); + await expect(todoItems).toHaveClass(['completed', '']); + + // Ensure there is 1 completed item. + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + + // Now reload. + await page.reload(); + await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); + await expect(firstTodoCheck).toBeChecked(); + await expect(todoItems).toHaveClass(['completed', '']); + }); +}); + +test.describe('Routing', () => { + test.beforeEach(async ({ page }) => { + await createDefaultTodos(page); + // make sure the app had a chance to save updated todos in storage + // before navigating to a new view, otherwise the items can get lost :( + // in some frameworks like Durandal + await checkTodosInLocalStorage(page, TODO_ITEMS[0]); + }); + + test('should allow me to display active items', async ({ page }) => { + const todoItem = page.getByTestId('todo-item'); + await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); + + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + await page.getByRole('link', { name: 'Active' }).click(); + await expect(todoItem).toHaveCount(2); + await expect(todoItem).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); + }); + + test('should respect the back button', async ({ page }) => { + const todoItem = page.getByTestId('todo-item'); + await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); + + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + + await test.step('Showing all items', async () => { + await page.getByRole('link', { name: 'All' }).click(); + await expect(todoItem).toHaveCount(3); + }); + + await test.step('Showing active items', async () => { + await page.getByRole('link', { name: 'Active' }).click(); + }); + + await test.step('Showing completed items', async () => { + await page.getByRole('link', { name: 'Completed' }).click(); + }); + + await expect(todoItem).toHaveCount(1); + await page.goBack(); + await expect(todoItem).toHaveCount(2); + await page.goBack(); + await expect(todoItem).toHaveCount(3); + }); + + test('should allow me to display completed items', async ({ page }) => { + await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + await page.getByRole('link', { name: 'Completed' }).click(); + await expect(page.getByTestId('todo-item')).toHaveCount(1); + }); + + test('should allow me to display all items', async ({ page }) => { + await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + await page.getByRole('link', { name: 'Active' }).click(); + await page.getByRole('link', { name: 'Completed' }).click(); + await page.getByRole('link', { name: 'All' }).click(); + await expect(page.getByTestId('todo-item')).toHaveCount(3); + }); + + test('should highlight the currently applied filter', async ({ page }) => { + await expect(page.getByRole('link', { name: 'All' })).toHaveClass('selected'); + + //create locators for active and completed links + const activeLink = page.getByRole('link', { name: 'Active' }); + const completedLink = page.getByRole('link', { name: 'Completed' }); + await activeLink.click(); + + // Page change - active items. + await expect(activeLink).toHaveClass('selected'); + await completedLink.click(); + + // Page change - completed items. + await expect(completedLink).toHaveClass('selected'); + }); +}); + +async function createDefaultTodos(page: Page) { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + for (const item of TODO_ITEMS) { + await newTodo.fill(item); + await newTodo.press('Enter'); + } +} + +async function checkNumberOfTodosInLocalStorage(page: Page, expected: number) { + return await page.waitForFunction(e => { + return JSON.parse(localStorage['react-todos']).length === e; + }, expected); +} + +async function checkNumberOfCompletedTodosInLocalStorage(page: Page, expected: number) { + return await page.waitForFunction(e => { + return JSON.parse(localStorage['react-todos']).filter((todo: any) => todo.completed).length === e; + }, expected); +} + +async function checkTodosInLocalStorage(page: Page, title: string) { + return await page.waitForFunction(t => { + return JSON.parse(localStorage['react-todos']).map((todo: any) => todo.title).includes(t); + }, title); +} diff --git a/frontend/cluesheet-web/tests/00_join_form_meshdb_500.spec.ts b/frontend/cluesheet-web/tests/00_join_form_meshdb_500.spec.ts new file mode 100644 index 0000000..0d33bb7 --- /dev/null +++ b/frontend/cluesheet-web/tests/00_join_form_meshdb_500.spec.ts @@ -0,0 +1,74 @@ +import { test, expect } from "./mock_meshdb_500/test"; +import { + fillOutJoinForm, + sampleData, + sampleJoinRecord, + submitSuccessExpected, +} from "./util"; +import { JoinRecord } from "@/lib/types"; +import { getJoinRecordFromS3 } from "@/lib/join_record"; +import { isDeepStrictEqual } from "util"; + +const joinFormTimeout = 20000; +const unitTestTimeout = 5000; + +const meshdbIsDownText = + "You will receive an email from us in the next 2-3 days with next steps, including how to submit panorama photos."; + +test("meshdb is 500ing but succeed anyway", async ({ page }) => { + test.setTimeout(joinFormTimeout); + await page.goto("/join"); + + // Is the page title correct? + await expect(page).toHaveTitle(/Join Our Community Network!/); + + // Set up sample data. + await fillOutJoinForm(page, sampleData); + + // Uncomment this if you want to poke around after the join form has been filled out + //await page.pause(); + + await submitSuccessExpected(page, unitTestTimeout); + + await expect(page.locator("[id='p-thank-you-01']")).toHaveText( + meshdbIsDownText, + ); + + const joinRecordKey = await page.getAttribute( + '[data-testid="test-join-record-key"]', + "data-state", + ); + + if (joinRecordKey === null) { + throw new Error("Got null join record"); + } + + // This is wacky because playwright has to run this and therefore + // needs our dotenv. + const joinRecord: JoinRecord = await getJoinRecordFromS3(joinRecordKey); + + // Crappy hack + joinRecord.submission_time = sampleJoinRecord.submission_time; + joinRecord.uuid = sampleJoinRecord.uuid; + + // In this case, we know that we won't have a code or install number, so drop + // those from the comparison + let hardDownSampleJoinRecord = structuredClone(sampleJoinRecord); + hardDownSampleJoinRecord.code = 500; + hardDownSampleJoinRecord.install_number = null; + + if (!isDeepStrictEqual(joinRecord, hardDownSampleJoinRecord)) { + console.error("Expected:"); + console.error(hardDownSampleJoinRecord); + console.error("Got:"); + console.error(joinRecord); + throw new Error("Bad JoinRecord. JoinRecord is not deeply equal."); + } + + // Then go home + await page.waitForTimeout(1000); + await page.locator("[name='home']").click(); + await page.waitForTimeout(1000); + const currentURL = new URL(page.url()); + expect(currentURL.pathname).toBe("/"); +}); diff --git a/frontend/cluesheet-web/tests/01_join_form_meshdb_down.spec.ts b/frontend/cluesheet-web/tests/01_join_form_meshdb_down.spec.ts new file mode 100644 index 0000000..c151f49 --- /dev/null +++ b/frontend/cluesheet-web/tests/01_join_form_meshdb_down.spec.ts @@ -0,0 +1,77 @@ +import { test, expect } from "@playwright/test"; +import { + fillOutJoinForm, + sampleData, + sampleJoinRecord, + submitFailureExpected, + submitSuccessExpected, +} from "./util"; +import { JoinRecord } from "@/lib/types"; +import { getJoinRecordFromS3 } from "@/lib/join_record"; +import { isDeepStrictEqual } from "util"; + +const joinFormTimeout = 20000; +const unitTestTimeout = 5000; + +const meshdbIsDownText = + "You will receive an email from us in the next 2-3 days with next steps, including how to submit panorama photos."; + +test("meshdb is hard down but succeed anyway", async ({ page }) => { + // Block access to the join form API + await page.route("**/api/v1/join/**", (route) => route.abort()); + + test.setTimeout(joinFormTimeout); + await page.goto("/join"); + + // Is the page title correct? + await expect(page).toHaveTitle(/Join Our Community Network!/); + + // Set up sample data. + await fillOutJoinForm(page, sampleData); + + // Uncomment this if you want to poke around after the join form has been filled out + //await page.pause(); + + await submitSuccessExpected(page, unitTestTimeout); + + await expect(page.locator("[id='p-thank-you-01']")).toHaveText( + meshdbIsDownText, + ); + + const joinRecordKey = await page.getAttribute( + '[data-testid="test-join-record-key"]', + "data-state", + ); + + if (joinRecordKey === null) { + throw new Error("Got null join record"); + } + + // This is wacky because playwright has to run this and therefore + // needs our dotenv. + const joinRecord: JoinRecord = await getJoinRecordFromS3(joinRecordKey); + + joinRecord.submission_time = sampleJoinRecord.submission_time; + joinRecord.uuid = sampleJoinRecord.uuid; + + // In this case, we know that we won't have a code or install number, so drop + // those from the comparison + let hardDownSampleJoinRecord = structuredClone(sampleJoinRecord); + hardDownSampleJoinRecord.code = null; + hardDownSampleJoinRecord.install_number = null; + + if (!isDeepStrictEqual(joinRecord, hardDownSampleJoinRecord)) { + console.error("Expected:"); + console.error(hardDownSampleJoinRecord); + console.error("Got:"); + console.error(joinRecord); + throw new Error("Bad JoinRecord. JoinRecord is not deeply equal."); + } + + // Then go home + await page.waitForTimeout(1000); + await page.locator("[name='home']").click(); + await page.waitForTimeout(1000); + const currentURL = new URL(page.url()); + expect(currentURL.pathname).toBe("/"); +}); diff --git a/frontend/cluesheet-web/tests/02_join_form.spec.ts b/frontend/cluesheet-web/tests/02_join_form.spec.ts new file mode 100644 index 0000000..4982160 --- /dev/null +++ b/frontend/cluesheet-web/tests/02_join_form.spec.ts @@ -0,0 +1,630 @@ +import { getJoinRecordFromS3 } from "@/lib/join_record"; +import { JoinRecord } from "@/lib/types"; +import { JoinFormValues } from "@/components/JoinForm/JoinForm"; +import { test, expect } from "./mock/test"; + +import { + sampleData, + fillOutJoinForm, + submitSuccessExpected, + submitFailureExpected, + submitConfirmationDialogExpected, + sampleNJData, + submitAndCheckToast, + expectSuccess, + sampleJoinRecord, + findJoinRecord, + chomSt, +} from "./util"; +import { isDeepStrictEqual } from "util"; + +const joinFormTimeout = 20000; +const unitTestTimeout = 5000; + +// Unit tests for the Join Form. +// +// These tests will mock a connection to MeshDB. It is simply making sure that +// the form creates a good-looking payload and can hit a mock API. + +test("change language from english to spanish", async ({ page }) => { + test.setTimeout(20000); + await page.goto("/join"); + + // Is the page title correct? + await expect(page).toHaveTitle(/Join Our Community Network!/); + + await page + .locator("[id='joinform-locale-switcher-select']") + .selectOption("🇪🇸 Español"); + + // TODO (wdn): It would be nice if we could grab the translation out of the + // json blob instead of hardcoding it here. + await expect(page.locator("[id='joinform-title']")).toHaveText( + "Únase NYC Mesh", + ); + + // Set up sample data. + await fillOutJoinForm(page, sampleData); + + await submitSuccessExpected(page, unitTestTimeout); + await expect(page.locator("[id='alert-thank-you-h2']")).toHaveText( + "¡Gracias! Por favor revisa su correo electronico.", + ); +}); + +test("happy join form", async ({ page }) => { + test.setTimeout(joinFormTimeout); + await page.goto("/join"); + + // Is the page title correct? + await expect(page).toHaveTitle(/Join Our Community Network!/); + + // Set up sample data. + await fillOutJoinForm(page, sampleData); + + // Uncomment this if you want to poke around after the join form has been filled out + //await page.pause(); + + await submitSuccessExpected(page, unitTestTimeout); + + // Hardcoding the translation messages to ensure that translation is actually working. + // If it breaks, it'll display the message key instead. + const thankYouTitle = "Thanks! Please check your email."; + const thankYouText = + "You will receive an email from us in the next 5-10 minutes with next steps, including how to submit panorama photos."; + + await expect(page.locator("[id='alert-thank-you-h2']")).toHaveText( + thankYouTitle, + ); + await expect(page.locator("[id='p-thank-you-01']")).toHaveText(thankYouText); + + const joinRecordKey = await page.getAttribute( + '[data-testid="test-join-record-key"]', + "data-state", + ); + + if (joinRecordKey === null) { + throw new Error("Got null join record"); + } + + // This is wacky because playwright has to run this and therefore + // needs our dotenv. + const joinRecord: JoinRecord = await getJoinRecordFromS3(joinRecordKey); + + // This is fucked, sorry. + joinRecord.submission_time = sampleJoinRecord.submission_time; + joinRecord.uuid = sampleJoinRecord.uuid; + + if (!isDeepStrictEqual(joinRecord, sampleJoinRecord)) { + console.error("Expected:"); + console.error(sampleJoinRecord); + console.error("Got:"); + console.error(joinRecord); + throw new Error("Bad JoinRecord. JoinRecord is not deeply equal."); + } + + // Then go home + await page.waitForTimeout(1000); + await page.locator("[name='home']").click(); + await page.waitForTimeout(1000); + const currentURL = new URL(page.url()); + expect(currentURL.pathname).toBe("/"); +}); + +// Tests missing phone +test("happy join form missing phone", async ({ page }) => { + test.setTimeout(joinFormTimeout); + await page.goto("/join"); + + // Is the page title correct? + await expect(page).toHaveTitle(/Join Our Community Network!/); + + let missingData: JoinFormValues = Object.assign({}, sampleData); + missingData.phone_number = ""; + + // Set up sample data. + await fillOutJoinForm(page, missingData); + await submitSuccessExpected(page, unitTestTimeout); +}); + +test("confirm city", async ({ page }) => { + test.setTimeout(joinFormTimeout); + await page.goto("/join"); + + // Is the page title correct? + await expect(page).toHaveTitle(/Join Our Community Network!/); + let data: JoinFormValues = Object.assign({}, sampleData); + data.city = "brooklyn"; + + // Set up sample data. + await fillOutJoinForm(page, data); + + // Uncomment this if you want to poke around after the join form has been filled out + //await page.pause(); + + await submitConfirmationDialogExpected(page, 2000); + + // Check that the Join Record's code is correct. + let joinRecord = await findJoinRecord(page); + let code = 409; + if (joinRecord.code !== code) { + throw new Error( + `JoinRecord code (${joinRecord.code}) did not match expected code (${code})`, + ); + } + + await page.locator("[name='confirm']").click(); + + // Make sure the JoinRecord updated properly + await page.waitForTimeout(1000); + joinRecord = await findJoinRecord(page); + if (joinRecord.code !== 201) { + throw new Error( + `JoinRecord code (${joinRecord.code}) did not match expected code (201)`, + ); + } + + await expectSuccess(page, 1000); +}); + +test("confirm street address", async ({ page }) => { + test.setTimeout(joinFormTimeout); + await page.goto("/join"); + + // Is the page title correct? + await expect(page).toHaveTitle(/Join Our Community Network!/); + let data = structuredClone(sampleData); + data.street_address = "197 prospect pl"; + + // Set up sample data. + await fillOutJoinForm(page, data); + + // Uncomment this if you want to poke around after the join form has been filled out + //await page.pause(); + + await submitConfirmationDialogExpected(page, 2000); + + await page.locator("[name='confirm']").click(); + + await expectSuccess(page, unitTestTimeout); +}); + +test("street address trust me bro", async ({ page }) => { + test.setTimeout(joinFormTimeout); + await page.goto("/join"); + + // Is the page title correct? + await expect(page).toHaveTitle(/Join Our Community Network!/); + let data: JoinFormValues = Object.assign({}, sampleData); + data.street_address = chomSt; + + // Set up sample data. + await fillOutJoinForm(page, data); + + // Uncomment this if you want to poke around after the join form has been filled out + //await page.pause(); + + await submitConfirmationDialogExpected(page, 2000); + + await page.locator("[name='reject']").click(); + + await expectSuccess(page, unitTestTimeout); +}); + +// TODO: Add a garbage testcase +// TODO: Add confirm block (is this trust me bro?) + +// Tests missing both first and last name +test("fail missing first name", async ({ page }) => { + test.setTimeout(joinFormTimeout); + await page.goto("/join"); + + // Is the page title correct? + await expect(page).toHaveTitle(/Join Our Community Network!/); + + let missingNameData: JoinFormValues = Object.assign({}, sampleData); + missingNameData.first_name = ""; + + // Set up sample data. + await fillOutJoinForm(page, missingNameData); + + await page.locator("[name='submit_join_form']").click(); + + await expect(page.locator("[name='submit_join_form']")).toBeEnabled(); + await expect(page.locator("[id='error_first_name']")).toBeVisible(); +}); + +test("fail missing last name", async ({ page }) => { + test.setTimeout(joinFormTimeout); + await page.goto("/join"); + + // Is the page title correct? + await expect(page).toHaveTitle(/Join Our Community Network!/); + + // Do it again with last name + let missingNameData: JoinFormValues = Object.assign({}, sampleData); + missingNameData.last_name = ""; + await fillOutJoinForm(page, missingNameData); + + await page.locator("[name='submit_join_form']").click(); + + await expect(page.locator("[name='submit_join_form']")).toBeEnabled(); + await expect(page.locator("[id='error_last_name']")).toBeVisible(); +}); + +// Tests missing email +test("fail missing email", async ({ page }) => { + test.setTimeout(joinFormTimeout); + await page.goto("/join"); + + // Is the page title correct? + await expect(page).toHaveTitle(/Join Our Community Network!/); + + let missingData: JoinFormValues = Object.assign({}, sampleData); + missingData.email_address = ""; + + // Set up sample data. + await fillOutJoinForm(page, missingData); + + await page.locator("[name='submit_join_form']").click(); + + await expect(page.locator("[name='submit_join_form']")).toBeEnabled(); + await expect(page.locator("[id='error_email_address']")).toBeVisible(); +}); + +// Tests missing email + phone +test("fail missing email and phone", async ({ page }) => { + test.setTimeout(joinFormTimeout); + await page.goto("/join"); + + // Is the page title correct? + await expect(page).toHaveTitle(/Join Our Community Network!/); + + let missingData: JoinFormValues = Object.assign({}, sampleData); + missingData.phone_number = ""; + missingData.email_address = ""; + + // Set up sample data. + await fillOutJoinForm(page, missingData); + + await page.locator("[name='submit_join_form']").click(); + + await expect(page.locator("[name='submit_join_form']")).toBeEnabled(); + await expect(page.locator("[id='error_email_address']")).toBeVisible(); + await expect(page.locator("[id='error_phone_number']")).toBeHidden(); +}); + +// Give a bad email address +test("fail bad email", async ({ page }) => { + test.setTimeout(joinFormTimeout); + await page.goto("/join"); + + // Is the page title correct? + await expect(page).toHaveTitle(/Join Our Community Network!/); + + let missingData: JoinFormValues = Object.assign({}, sampleData); + missingData.email_address = "notagoodemail"; + + // Set up sample data. + await fillOutJoinForm(page, missingData); + + await page.locator("[name='submit_join_form']").click(); + + await expect(page.locator("[name='submit_join_form']")).toBeEnabled(); + await expect(page.locator("[id='error_email_address']")).toBeVisible(); +}); + +test("fail bad email 2", async ({ page }) => { + test.setTimeout(joinFormTimeout); + await page.goto("/join"); + + // Is the page title correct? + await expect(page).toHaveTitle(/Join Our Community Network!/); + + let missingData: JoinFormValues = Object.assign({}, sampleData); + + // This email is bad, in way that our basic validation doesn't catch but the backend does + missingData.email_address = "a@b"; + await fillOutJoinForm(page, missingData); + await submitFailureExpected(page); +}); + +// Tests bad phone +test("fail bad phone", async ({ page }) => { + test.setTimeout(joinFormTimeout); + await page.goto("/join"); + + // Is the page title correct? + await expect(page).toHaveTitle(/Join Our Community Network!/); + + let missingData: JoinFormValues = Object.assign({}, sampleData); + missingData.phone_number = "twelve"; + + // Set up sample data. + await fillOutJoinForm(page, missingData); + + await page.locator("[name='submit_join_form']").click(); + + await expect(page.locator("[name='submit_join_form']")).toBeEnabled(); + await expect(page.locator("[id='error_phone_number']")).toBeVisible(); +}); + +test("fail bad phone 2", async ({ page }) => { + test.setTimeout(joinFormTimeout); + await page.goto("/join"); + + // Is the page title correct? + await expect(page).toHaveTitle(/Join Our Community Network!/); + + let missingData: JoinFormValues = Object.assign({}, sampleData); + missingData.phone_number = "12"; + await fillOutJoinForm(page, missingData); + await submitFailureExpected(page); +}); + +test("fail missing address", async ({ page }) => { + test.setTimeout(joinFormTimeout); + await page.goto("/join"); + + // Is the page title correct? + await expect(page).toHaveTitle(/Join Our Community Network!/); + + let missingAddressData: JoinFormValues = Object.assign({}, sampleData); + missingAddressData.street_address = ""; + await fillOutJoinForm(page, missingAddressData); + + await page.locator("[name='submit_join_form']").click(); + + await expect(page.locator("[name='submit_join_form']")).toBeEnabled(); + await expect(page.locator("[id='error_street_address']")).toBeVisible(); +}); + +test("fail missing city", async ({ page }) => { + test.setTimeout(joinFormTimeout); + await page.goto("/join"); + + // Is the page title correct? + await expect(page).toHaveTitle(/Join Our Community Network!/); + + let missingAddressData: JoinFormValues = Object.assign({}, sampleData); + missingAddressData.city = ""; + await fillOutJoinForm(page, missingAddressData); + + await page.locator("[name='submit_join_form']").click(); + + await expect(page.locator("[name='submit_join_form']")).toBeEnabled(); + await expect(page.locator("[id='error_city']")).toBeVisible(); +}); + +// This one should fail and here's why: It's really annoying when people +// don't give us their apartment #, so we make it required at the expense +// of those who live in houses. They can just write "house" or "N/A" or +// something +// TODO (wdn): Add a checkbox for my house-havers +test("fail missing unit number", async ({ page }) => { + test.setTimeout(joinFormTimeout); + await page.goto("/join"); + + // Is the page title correct? + await expect(page).toHaveTitle(/Join Our Community Network!/); + + let missingAddressData: JoinFormValues = Object.assign({}, sampleData); + missingAddressData.apartment = ""; + await fillOutJoinForm(page, missingAddressData); + + await page.locator("[name='submit_join_form']").click(); + + await expect(page.locator("[name='submit_join_form']")).toBeEnabled(); + await expect(page.locator("[id='error_apartment']")).toBeVisible(); +}); + +test("fail nj", async ({ page }) => { + test.setTimeout(joinFormTimeout); + await page.goto("/join"); + + // Is the page title correct? + await expect(page).toHaveTitle(/Join Our Community Network!/); + + // Set up sample data. + await fillOutJoinForm(page, sampleNJData); + + // Uncomment this if you want to poke around after the join form has been filled out + //await page.pause(); + + await submitAndCheckToast( + page, + "Non-NYC registrations are not supported at this time", + ); + + // Check that the Join Record's code is correct. + let joinRecord = await findJoinRecord(page); + let code = 400; + if (joinRecord.code !== code) { + throw new Error( + `JoinRecord code (${joinRecord.code}) did not match expected code (${code})`, + ); + } +}); + +test.describe("user triggered captchaV2", () => { + test.skip(process.env.RUN_CAPTCHA !== "true"); + test("user triggered captchaV2", async ({ page }) => { + test.setTimeout(joinFormTimeout); + await page.goto("/join"); + + // Is the page title correct? + await expect(page).toHaveTitle(/Join Our Community Network!/); + + // Set up sample data. + let botTriggeringData: JoinFormValues = Object.assign({}, sampleData); + + await fillOutJoinForm(page, botTriggeringData); + + await submitAndCheckToast( + page, + "Please complete an additional verification step to confirm your submission", + ); + + await page.waitForTimeout(1000); + + // Make the robot check the "I'm not a robot" button (commit voter fraud) + await page + .locator("[title='reCAPTCHA']") + .nth(1) + .contentFrame() + .locator("[id='recaptcha-anchor']") + .click(); + + await submitSuccessExpected(page, unitTestTimeout); + }); + + test("user triggered captchaV2 and trust me bro", async ({ page }) => { + test.setTimeout(joinFormTimeout * 2); // This is a really long test + await page.goto("/join"); + + // Is the page title correct? + await expect(page).toHaveTitle(/Join Our Community Network!/); + + // Set up sample data. + let botTriggeringData: JoinFormValues = Object.assign({}, sampleData); + botTriggeringData.street_address = chomSt; + + await fillOutJoinForm(page, botTriggeringData); + + await submitAndCheckToast( + page, + "Please complete an additional verification step to confirm your submission", + ); + + await page.waitForTimeout(1000); + + // Make the robot check the "I'm not a robot" button (commit voter fraud) + await page + .locator("[title='reCAPTCHA']") + .nth(1) + .contentFrame() + .locator("[id='recaptcha-anchor']") + .click(); + + await submitConfirmationDialogExpected(page, 2000); + + // 2 counts of voter fraud + await page + .locator("[title='reCAPTCHA']") + .nth(2) + .contentFrame() + .locator("[id='recaptcha-anchor']") + .click(); + + await page.locator("[name='confirm']").click(); + + await expectSuccess(page, unitTestTimeout); + }); + + test("user triggered captchaV2 and trust me bro and reject changes", async ({ + page, + }) => { + test.setTimeout(joinFormTimeout * 2); // This is a really long test + await page.goto("/join"); + + // Is the page title correct? + await expect(page).toHaveTitle(/Join Our Community Network!/); + + // Set up sample data. + let botTriggeringData: JoinFormValues = Object.assign({}, sampleData); + botTriggeringData.street_address = chomSt; + + await fillOutJoinForm(page, botTriggeringData); + + await submitAndCheckToast( + page, + "Please complete an additional verification step to confirm your submission", + ); + + await page.waitForTimeout(1000); + + // Make the robot check the "I'm not a robot" button (commit voter fraud) + await page + .locator("[title='reCAPTCHA']") + .nth(1) + .contentFrame() + .locator("[id='recaptcha-anchor']") + .click(); + + await submitConfirmationDialogExpected(page, 2000); + + // 2 counts of voter fraud + await page + .locator("[title='reCAPTCHA']") + .nth(2) + .contentFrame() + .locator("[id='recaptcha-anchor']") + .click(); + + await page.locator("[name='reject']").click(); + + await expectSuccess(page, unitTestTimeout); + }); + + test("user triggered captchaV2 and trust me bro and cancel and try again", async ({ + page, + }) => { + test.setTimeout(joinFormTimeout * 2); // This is a really long test + await page.goto("/join"); + + // Is the page title correct? + await expect(page).toHaveTitle(/Join Our Community Network!/); + + // Set up sample data. + let botTriggeringData: JoinFormValues = Object.assign({}, sampleData); + botTriggeringData.street_address = chomSt; + + // Fill out the form with our data + await fillOutJoinForm(page, botTriggeringData); + + // Expect a warning asking us to do a captcha + await submitAndCheckToast( + page, + "Please complete an additional verification step to confirm your submission", + ); + + // Make the robot check the "I'm not a robot" button (commit voter fraud) + await page.waitForTimeout(1000); + await page + .locator("[title='reCAPTCHA']") + .nth(1) + .contentFrame() + .locator("[id='recaptcha-anchor']") + .click(); + + // Expect the dialogue to show up + await submitConfirmationDialogExpected(page, 2000); + + // dismiss it + await page.waitForTimeout(1000); + await page.locator("[name='cancel']").click(); + + // Do the captcha again + await page + .locator("[title='reCAPTCHA']") + .nth(1) + .contentFrame() + .locator("[id='recaptcha-anchor']") + .click(); + + // Try submitting again + await submitConfirmationDialogExpected(page, 2000); + + // 3 counts of voter fraud + await page + .locator("[title='reCAPTCHA']") + .nth(2) + .contentFrame() + .locator("[id='recaptcha-anchor']") + .click(); + + await page.locator("[name='confirm']").click(); + + await expectSuccess(page, unitTestTimeout); + }); +}); diff --git a/frontend/cluesheet-web/tests/03_nn_assign_form.spec.ts b/frontend/cluesheet-web/tests/03_nn_assign_form.spec.ts new file mode 100644 index 0000000..bf34fb8 --- /dev/null +++ b/frontend/cluesheet-web/tests/03_nn_assign_form.spec.ts @@ -0,0 +1,79 @@ +import { test, expect } from "./mock/test"; + +const timeout = 10000; +const clickTimeout = 1000; + +// Unit tests for the Join Form. +// +// These tests will mock a connection to MeshDB. It is simply making sure that +// the form creates a good-looking payload and can hit a mock API. + +test("nn assign happy", async ({ page }) => { + test.setTimeout(timeout); + await page.goto("/nn-assign"); + + await expect(page).toHaveTitle(/Assign a Network Number/); + + await page.getByPlaceholder("Install Number").fill("20000"); + await page.getByPlaceholder("Pre-Shared Key").fill("localdev"); + + await page.getByRole("button", { name: /Submit/i }).click(); + await page.waitForTimeout(clickTimeout); + + await expect(page.locator("[id='alert-network-number']")).toBeVisible(); + await expect(page.locator("[id='assigned-network-number']")).toHaveText( + "420", + ); +}); + +test("nn assign already assigned", async ({ page }) => { + test.setTimeout(timeout); + await page.goto("/nn-assign"); + + await expect(page).toHaveTitle(/Assign a Network Number/); + + await page.getByPlaceholder("Install Number").fill("30000"); + await page.getByPlaceholder("Pre-Shared Key").fill("localdev"); + + await page.getByRole("button", { name: /Submit/i }).click(); + await page.waitForTimeout(clickTimeout); + + await expect(page.locator("[id='alert-network-number']")).toBeVisible(); + await expect(page.locator("[id='nn-message']")).toHaveText( + "This Install Number (30000) already has a Network Number (520) associated with it!", + ); + await expect(page.locator("[id='assigned-network-number']")).toHaveText( + "520", + ); +}); + +test("nn assign wrong password", async ({ page }) => { + test.setTimeout(timeout); + await page.goto("/nn-assign"); + + await expect(page).toHaveTitle(/Assign a Network Number/); + + await page.getByPlaceholder("Install Number").fill("20000"); + await page.getByPlaceholder("Pre-Shared Key").fill("badpassword"); + + await page.getByRole("button", { name: /Submit/i }).click(); + await page.waitForTimeout(clickTimeout); + + await expect(page.locator("[id='alert-network-number']")).toBeHidden(); + await expect(page.locator("[id='assigned-network-number']")).toHaveText(""); +}); + +test("nn assign no password", async ({ page }) => { + test.setTimeout(timeout); + await page.goto("/nn-assign"); + + await expect(page).toHaveTitle(/Assign a Network Number/); + + await page.getByPlaceholder("Install Number").fill("20000"); + + await expect(page.getByRole("button", { name: /Submit/i })).toBeDisabled(); + await page.waitForTimeout(clickTimeout); + + await expect(page.locator("[id='alert-network-number']")).toBeHidden(); + await expect(page.locator("[id='assigned-network-number']")).toHaveText(""); +}); diff --git a/frontend/cluesheet-web/tests/04_join_form_s3_down.spec.ts b/frontend/cluesheet-web/tests/04_join_form_s3_down.spec.ts new file mode 100644 index 0000000..f51d75c --- /dev/null +++ b/frontend/cluesheet-web/tests/04_join_form_s3_down.spec.ts @@ -0,0 +1,30 @@ +import { test, expect } from "@playwright/test"; +import { fillOutJoinForm, sampleData, submitFailureExpected } from "./util"; + +const joinFormTimeout = 20000; + +// This test will run in its own github action that does not set up MinIO. +// I tried for WEEKS and I could not find a way to mock a failure between +// Node and MinIO. +test.describe("fail meshdb is hard down and s3 is hard down", () => { + test.skip(process.env.RUN_SKIPPED !== "true"); + + test("fail meshdb is hard down and s3 is hard down", async ({ page }) => { + // Block access to the join form API + await page.route("**/api/v1/join/**", (route) => route.abort()); + + test.setTimeout(joinFormTimeout); + await page.goto("/join"); + + // Is the page title correct? + await expect(page).toHaveTitle(/Join Our Community Network!/); + + // Set up sample data. + await fillOutJoinForm(page, sampleData); + + // Uncomment this if you want to poke around after the join form has been filled out + //await page.pause(); + + await submitFailureExpected(page); + }); +}); diff --git a/frontend/cluesheet-web/tests/05_join_form_i18n.spec.ts b/frontend/cluesheet-web/tests/05_join_form_i18n.spec.ts new file mode 100644 index 0000000..26c1985 --- /dev/null +++ b/frontend/cluesheet-web/tests/05_join_form_i18n.spec.ts @@ -0,0 +1,45 @@ +import { JoinFormValues } from "@/components/JoinForm/JoinForm"; +import { test, expect } from "./mock/test"; + +import { + sampleData, + fillOutJoinForm, + submitConfirmationDialogExpected, + expectSuccess, +} from "./util"; + +const joinFormTimeout = 40000; +const unitTestTimeout = 5000; + +test("es confirm street address", async ({ page }) => { + test.setTimeout(joinFormTimeout); + await page.goto("/join"); + + // Is the page title correct? + await expect(page).toHaveTitle(/Join Our Community Network!/); + + await page + .locator("[id='joinform-locale-switcher-select']") + .selectOption("🇪🇸 Español"); + + await expect(page.locator("[id='joinform-title']")).toHaveText( + "Únase NYC Mesh", + ); + + let data: JoinFormValues = Object.assign({}, sampleData); + data.street_address = "197 prospect pl"; + + // Set up sample data. + await fillOutJoinForm(page, data); + + await submitConfirmationDialogExpected(page, 2000); + + // Ensure the dialogue is translated + await expect(page.locator("[id='alert-dialog-description']")).toHaveText( + "Debíamos que reformatear algunos de sus datos. Por favor asegurar que los campos abajos son ascertados.", + ); + + await page.locator("[name='confirm']").click(); + + await expectSuccess(page, unitTestTimeout); +}); diff --git a/frontend/cluesheet-web/tests/mock/handlers.ts b/frontend/cluesheet-web/tests/mock/handlers.ts new file mode 100644 index 0000000..0e09b78 --- /dev/null +++ b/frontend/cluesheet-web/tests/mock/handlers.ts @@ -0,0 +1,187 @@ +import { http, HttpResponse } from "msw"; +import { chomSt, chomStreet, expectedAPIRequestData } from "../util"; +import { isDeepStrictEqual } from "util"; +import { + JoinFormResponse, + JoinFormValues, +} from "@/components/JoinForm/JoinForm"; +import { NNAssignRequestValues } from "@/components/NNAssignForm/NNAssignForm"; + +export default [ + http.post("/api/v1/join/", async ({ request }) => { + console.debug("Hello from mocked join API."); + + const requestJson = await request.json(); + + if (requestJson === undefined || requestJson === null) { + return HttpResponse.json( + { detail: "Mock: Missing request body" }, + { status: 400 }, + ); + } + + // First thing we should do is check captcha if it's enabled + if ( + request.headers.get("X-Recaptcha-V2-Token") === "" && + process.env.RECAPTCHA_V2_KEY + ) { + return HttpResponse.json( + { detail: "Captcha verification failed" }, + { status: 401 }, + ); + } + + const joinRequest: JoinFormValues = requestJson as JoinFormValues; + + const good_response: JoinFormResponse = { + detail: "", + building_id: "a5b316f2-c167-40a1-8f40-dd6b54daf0fe", + member_id: "c9305944-80d8-4b1c-b5b9-3c9c0db0d2a1", + install_id: "d9e13697-8240-4c0c-be86-d9aa67617165", + install_number: 1002, + member_exists: false, + changed_info: {}, + }; + + // Special case to test "trust me bro" + if (joinRequest.street_address === chomSt) { + if (joinRequest.trust_me_bro) { + return HttpResponse.json(good_response, { status: 201 }); + } + + // Else, we're gonna return a 409. + let r = new JoinFormResponse(); + r.detail = "Mock: Please confirm a few details."; + r.changed_info = { street_address: chomStreet }; + + return HttpResponse.json(r, { status: 409 }); + } + + if (!joinRequest.trust_me_bro) { + // Bail on New Jersey + if ( + joinRequest.state === "NJ" || + joinRequest.state === "New Jersey" || + joinRequest.zip_code === "07030" + ) { + let r = new JoinFormResponse(); + r.detail = + "Mock: Non-NYC registrations are not supported at this time."; + + return HttpResponse.json(r, { status: 400 }); + } + + // Return 409 if the street address is improperly formatted + if (joinRequest.street_address === "197 prospect pl") { + let r = new JoinFormResponse(); + r.detail = "Mock: Please confirm a few details."; + r.changed_info = { street_address: "197 Prospect Place" }; + + return HttpResponse.json(r, { status: 409 }); + } + + // Return 409 if the city is improperly formatted + if (joinRequest.city === "brooklyn") { + let r = new JoinFormResponse(); + r.detail = "Mock: Please confirm a few details."; + r.changed_info = { city: "Brooklyn" }; + return HttpResponse.json(r, { status: 409 }); + } + + if (joinRequest.phone_number === "") { + // This is probably the wrong way to do this, but the empty phone number case was + // failing the strict equality check below + + let expectedData: JoinFormValues = Object.assign( + {}, + expectedAPIRequestData, + ); + expectedData.phone_number = ""; + if (isDeepStrictEqual(joinRequest, expectedData)) { + return HttpResponse.json(good_response, { status: 201 }); + } + } + + // If anything else is wrong with the form we got, then bail + if (!isDeepStrictEqual(joinRequest, expectedAPIRequestData)) { + console.error( + "Mock Join API is returning 400. (request is not deeply equal to expectedAPIRequestData)", + ); + console.error("Expected the following:"); + console.error(expectedAPIRequestData); + console.error("Got the follwing:"); + console.error(joinRequest); + + let r = new JoinFormResponse(); + r.detail = "Mock failure. Request does not match expected request."; + return HttpResponse.json(r, { status: 400 }); + } + } + + if (joinRequest.street_address === "333 chom st") { + // wtf we're still here? bail!!! + // TODO: Re-write this mock server because it SUUUUCKS + let r = new JoinFormResponse(); + r.detail = "What the fuck"; + return HttpResponse.json(r, { status: 400 }); + } + + // OK we're chilling. Return 200 + return HttpResponse.json(good_response, { status: 201 }); + }), + http.post("/api/v1/nn-assign/", async ({ request }) => { + console.debug("Hello from mocked NN Assign API."); + + const requestJson = await request.json(); + + if (requestJson === undefined || requestJson === null) { + return HttpResponse.json( + { detail: "Mock: Missing request body" }, + { status: 400 }, + ); + } + + const nnAssignRequest: NNAssignRequestValues = + requestJson as NNAssignRequestValues; + + // Firstly, check if we have the right password + if (nnAssignRequest.password != "localdev") { + console.debug("Mock bad password"); + return HttpResponse.json( + { detail: "Mock failure. Authentication Failed." }, + { status: 400 }, + ); + } + + if (nnAssignRequest.install_number == "20000") { + const json = { + detail: "Network Number has been assigned!", + building_id: 69, + install_id: 69, + install_number: 20000, + network_number: 420, + created: true, + }; + + return HttpResponse.json(json, { status: 201 }); + } + + if (nnAssignRequest.install_number == "30000") { + const message = `This Install Number (30000) already has a Network Number (520) associated with it!`; + const json = { + detail: message, + building_id: 79, + install_id: 79, + install_number: 30000, + network_number: 520, + created: false, + }; + return HttpResponse.json(json, { status: 200 }); + } + + return HttpResponse.json( + { detail: "Mock failure. Server Error." }, + { status: 500 }, + ); + }), +]; diff --git a/frontend/cluesheet-web/tests/mock/test.ts b/frontend/cluesheet-web/tests/mock/test.ts new file mode 100644 index 0000000..f7c7d20 --- /dev/null +++ b/frontend/cluesheet-web/tests/mock/test.ts @@ -0,0 +1,16 @@ +import { test as base, expect } from "@playwright/test"; +import { http } from "msw"; +import type { MockServiceWorker } from "playwright-msw"; +import { createWorkerFixture } from "playwright-msw"; + +import handlers from "./handlers"; + +const test = base.extend<{ + worker: MockServiceWorker; + http: typeof http; +}>({ + worker: createWorkerFixture(handlers), + http, +}); + +export { expect, test }; diff --git a/frontend/cluesheet-web/tests/mock_meshdb_500/handlers.ts b/frontend/cluesheet-web/tests/mock_meshdb_500/handlers.ts new file mode 100644 index 0000000..8a8e55e --- /dev/null +++ b/frontend/cluesheet-web/tests/mock_meshdb_500/handlers.ts @@ -0,0 +1,21 @@ +import { http, HttpResponse } from "msw"; +import { JoinFormResponse } from "@/components/JoinForm/JoinForm"; + +export default [ + http.post("/api/v1/join/", async ({ request }) => { + console.debug("Hello from mocked join API. I'm gonna return a 500 now :)"); + + let r = new JoinFormResponse(); + r.detail = "Mock: A server error has occurred."; + + return HttpResponse.json(r, { status: 500 }); + }), + http.post("/api/v1/nn-assign/", async ({ request }) => { + console.debug("Hello from mocked NN Assign API."); + + return HttpResponse.json( + { detail: "Mock: A server error has occurred." }, + { status: 500 }, + ); + }), +]; diff --git a/frontend/cluesheet-web/tests/mock_meshdb_500/test.ts b/frontend/cluesheet-web/tests/mock_meshdb_500/test.ts new file mode 100644 index 0000000..f7c7d20 --- /dev/null +++ b/frontend/cluesheet-web/tests/mock_meshdb_500/test.ts @@ -0,0 +1,16 @@ +import { test as base, expect } from "@playwright/test"; +import { http } from "msw"; +import type { MockServiceWorker } from "playwright-msw"; +import { createWorkerFixture } from "playwright-msw"; + +import handlers from "./handlers"; + +const test = base.extend<{ + worker: MockServiceWorker; + http: typeof http; +}>({ + worker: createWorkerFixture(handlers), + http, +}); + +export { expect, test }; diff --git a/frontend/cluesheet-web/tests/util.ts b/frontend/cluesheet-web/tests/util.ts new file mode 100644 index 0000000..01c0d72 --- /dev/null +++ b/frontend/cluesheet-web/tests/util.ts @@ -0,0 +1,185 @@ +import { getJoinRecordFromS3 } from "@/lib/join_record"; +import { JoinRecord } from "@/lib/types"; +import { JoinFormValues } from "@/components/JoinForm/JoinForm"; +import { expect, Page } from "@playwright/test"; + +export const chomSt = "333 chom st"; +export const chomStreet = "333 Chom Street"; + +export const sampleData: JoinFormValues = { + first_name: "Jon", + last_name: "Smith", + email_address: "js@gmail.com", + phone_number: "585-475-2411", + street_address: "197 Prospect Place", + apartment: "1", + city: "Brooklyn", + state: "NY", + zip_code: "11238", + roof_access: true, + referral: "Mock Sample Data", + ncl: true, + trust_me_bro: false, +}; + +export const sampleJoinRecord: JoinRecord = { + first_name: "Jon", + last_name: "Smith", + email_address: "js@gmail.com", + phone_number: "+1 585-475-2411", + street_address: "197 Prospect Place", + apartment: "1", + city: "Brooklyn", + state: "NY", + zip_code: "11238", + roof_access: true, + referral: "Mock Sample Data", + ncl: true, + trust_me_bro: false, + version: 3, + uuid: "1a55b949-0490-4b78-a2e8-10aea41d6f1d", + submission_time: "2024-11-01T08:24:24", + code: 201, + replayed: 0, + install_number: 1002, +}; + +export const expectedTrustMeBroValues: JoinFormValues = { + first_name: "Jon", + last_name: "Smith", + email_address: "js@gmail.com", + phone_number: "585-475-2411", + street_address: "197 prospect pl", + apartment: "1", + city: "brooklyn", + state: "NY", + zip_code: "11238", + roof_access: true, + referral: "Mock Sample Data", + ncl: true, + trust_me_bro: false, +}; + +export const sampleNJData: JoinFormValues = { + first_name: "Jon", + last_name: "Smith", + email_address: "js@gmail.com", + phone_number: "585-475-2411", + street_address: "711 Hudson Street", + city: "Hoboken", + state: "NJ", + zip_code: "07030", + apartment: "1", + roof_access: true, + referral: "Mock Sample Data", + ncl: true, + trust_me_bro: false, +}; + +let expectedAPIRequestDataMut: JoinFormValues = sampleData; +expectedAPIRequestDataMut.phone_number = "+1 585-475-2411"; + +export const expectedAPIRequestData = expectedAPIRequestDataMut; + +export async function fillOutJoinForm(page: Page, sampleData: JoinFormValues) { + // Set up some sample data + + // Personal info + await page.locator("[name='first_name']").fill(sampleData.first_name); + await page.locator("[name='last_name']").fill(sampleData.last_name); + await page.locator("[name='email_address']").fill(sampleData.email_address); + await page.locator("[name='phone_number']").fill(sampleData.phone_number); + + // Address Info + await page.locator("[name='street_address']").fill(sampleData.street_address); + await page.locator("[name='apartment']").fill(sampleData.apartment); + await page.locator("[name='city']").fill(sampleData.city); + await page.locator("[name='state']").fill(sampleData.state); + await page.locator("[name='zip_code']").fill(sampleData.zip_code.toString()); + + // How did you hear about us? + await page.locator("[name='referral']").fill(sampleData.referral); + + // Agree to the NCL + if (sampleData.ncl) { + await page.locator("[name='ncl']").check(); + } + + // Roof Access + if (sampleData.roof_access) { + await page.locator("[name='roof_access']").check(); + } +} + +export async function submitFailureExpected(page: Page) { + await page.waitForTimeout(1000); + // Submit the join form + await page.locator("[name='submit_join_form']").click(); + + await page.waitForTimeout(1000); + + // The submission should've been stopped, and the member should be able to try again. + await expect(page.locator("[name='submit_join_form']")).toHaveText("Submit"); +} + +export async function submitAndCheckToast(page: Page, toastMessage: string) { + await page.waitForTimeout(1000); + // Submit the join form + await page.locator("[name='submit_join_form']").click(); + + await page.waitForTimeout(1000); + + // The submission should've been stopped, and the member should be able to try again. + await expect(page.locator("[name='submit_join_form']")).toHaveText("Submit"); + await expect(page.getByTestId("toasty")).toContainText(toastMessage); +} + +export async function submitSuccessExpected( + page: Page, + timeout: number = 10000, +) { + await page.waitForTimeout(1000); + // Listen for all console logs + page.on("console", (msg) => console.log(msg.text())); + + // Submit the join form + await page.locator("[name='submit_join_form']").click(); + + await expectSuccess(page, timeout); +} + +export async function expectSuccess(page: Page, timeout: number = 1000) { + await page.waitForTimeout(timeout); + await expect(page.locator("[name='submit_join_form']")).toBeHidden(); + await expect(page.locator("[id='alert-thank-you']")).toBeVisible(); +} + +export async function submitConfirmationDialogExpected( + page: Page, + timeout: number = 10000, +) { + // Listen for all console logs + page.on("console", (msg) => console.log(msg.text())); + + // Submit the join form + await page.locator("[name='submit_join_form']").click(); + + await page.waitForTimeout(timeout); + await expect(page.locator("[id='alert-dialog-title']")).toBeVisible(); +} + +export async function findJoinRecord(page: Page): Promise { + const joinRecordKey = await page.getAttribute( + '[data-testid="test-join-record-key"]', + "data-state", + ); + + if (joinRecordKey === null) { + throw new Error("Got null join record"); + } + + // This is wacky because playwright has to run this and therefore + // needs our dotenv. + const joinRecord: JoinRecord = await getJoinRecordFromS3(joinRecordKey); + return joinRecord; +} From f710695098173e86d456086b067cd0204cc12cc3 Mon Sep 17 00:00:00 2001 From: Will Nilges Date: Sun, 4 May 2025 02:06:13 -0400 Subject: [PATCH 04/12] delete unneeded stuff --- frontend/cluesheet-web/e2e/example.spec.ts | 18 - frontend/cluesheet-web/playwright.config.ts | 2 +- .../tests-examples/demo-todo-app.spec.ts | 437 ------------------ 3 files changed, 1 insertion(+), 456 deletions(-) delete mode 100644 frontend/cluesheet-web/e2e/example.spec.ts delete mode 100644 frontend/cluesheet-web/tests-examples/demo-todo-app.spec.ts diff --git a/frontend/cluesheet-web/e2e/example.spec.ts b/frontend/cluesheet-web/e2e/example.spec.ts deleted file mode 100644 index 54a906a..0000000 --- a/frontend/cluesheet-web/e2e/example.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { test, expect } from '@playwright/test'; - -test('has title', async ({ page }) => { - await page.goto('https://playwright.dev/'); - - // Expect a title "to contain" a substring. - await expect(page).toHaveTitle(/Playwright/); -}); - -test('get started link', async ({ page }) => { - await page.goto('https://playwright.dev/'); - - // Click the get started link. - await page.getByRole('link', { name: 'Get started' }).click(); - - // Expects page to have a heading with the name of Installation. - await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible(); -}); diff --git a/frontend/cluesheet-web/playwright.config.ts b/frontend/cluesheet-web/playwright.config.ts index efd9b88..a05d8b5 100644 --- a/frontend/cluesheet-web/playwright.config.ts +++ b/frontend/cluesheet-web/playwright.config.ts @@ -12,7 +12,7 @@ import { defineConfig, devices } from '@playwright/test'; * See https://playwright.dev/docs/test-configuration. */ export default defineConfig({ - testDir: './e2e', + 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. */ diff --git a/frontend/cluesheet-web/tests-examples/demo-todo-app.spec.ts b/frontend/cluesheet-web/tests-examples/demo-todo-app.spec.ts deleted file mode 100644 index 8641cb5..0000000 --- a/frontend/cluesheet-web/tests-examples/demo-todo-app.spec.ts +++ /dev/null @@ -1,437 +0,0 @@ -import { test, expect, type Page } from '@playwright/test'; - -test.beforeEach(async ({ page }) => { - await page.goto('https://demo.playwright.dev/todomvc'); -}); - -const TODO_ITEMS = [ - 'buy some cheese', - 'feed the cat', - 'book a doctors appointment' -] as const; - -test.describe('New Todo', () => { - test('should allow me to add todo items', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - // Create 1st todo. - await newTodo.fill(TODO_ITEMS[0]); - await newTodo.press('Enter'); - - // Make sure the list only has one todo item. - await expect(page.getByTestId('todo-title')).toHaveText([ - TODO_ITEMS[0] - ]); - - // Create 2nd todo. - await newTodo.fill(TODO_ITEMS[1]); - await newTodo.press('Enter'); - - // Make sure the list now has two todo items. - await expect(page.getByTestId('todo-title')).toHaveText([ - TODO_ITEMS[0], - TODO_ITEMS[1] - ]); - - await checkNumberOfTodosInLocalStorage(page, 2); - }); - - test('should clear text input field when an item is added', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - // Create one todo item. - await newTodo.fill(TODO_ITEMS[0]); - await newTodo.press('Enter'); - - // Check that input is empty. - await expect(newTodo).toBeEmpty(); - await checkNumberOfTodosInLocalStorage(page, 1); - }); - - test('should append new items to the bottom of the list', async ({ page }) => { - // Create 3 items. - await createDefaultTodos(page); - - // create a todo count locator - const todoCount = page.getByTestId('todo-count') - - // Check test using different methods. - await expect(page.getByText('3 items left')).toBeVisible(); - await expect(todoCount).toHaveText('3 items left'); - await expect(todoCount).toContainText('3'); - await expect(todoCount).toHaveText(/3/); - - // Check all items in one call. - await expect(page.getByTestId('todo-title')).toHaveText(TODO_ITEMS); - await checkNumberOfTodosInLocalStorage(page, 3); - }); -}); - -test.describe('Mark all as completed', () => { - test.beforeEach(async ({ page }) => { - await createDefaultTodos(page); - await checkNumberOfTodosInLocalStorage(page, 3); - }); - - test.afterEach(async ({ page }) => { - await checkNumberOfTodosInLocalStorage(page, 3); - }); - - test('should allow me to mark all items as completed', async ({ page }) => { - // Complete all todos. - await page.getByLabel('Mark all as complete').check(); - - // Ensure all todos have 'completed' class. - await expect(page.getByTestId('todo-item')).toHaveClass(['completed', 'completed', 'completed']); - await checkNumberOfCompletedTodosInLocalStorage(page, 3); - }); - - test('should allow me to clear the complete state of all items', async ({ page }) => { - const toggleAll = page.getByLabel('Mark all as complete'); - // Check and then immediately uncheck. - await toggleAll.check(); - await toggleAll.uncheck(); - - // Should be no completed classes. - await expect(page.getByTestId('todo-item')).toHaveClass(['', '', '']); - }); - - test('complete all checkbox should update state when items are completed / cleared', async ({ page }) => { - const toggleAll = page.getByLabel('Mark all as complete'); - await toggleAll.check(); - await expect(toggleAll).toBeChecked(); - await checkNumberOfCompletedTodosInLocalStorage(page, 3); - - // Uncheck first todo. - const firstTodo = page.getByTestId('todo-item').nth(0); - await firstTodo.getByRole('checkbox').uncheck(); - - // Reuse toggleAll locator and make sure its not checked. - await expect(toggleAll).not.toBeChecked(); - - await firstTodo.getByRole('checkbox').check(); - await checkNumberOfCompletedTodosInLocalStorage(page, 3); - - // Assert the toggle all is checked again. - await expect(toggleAll).toBeChecked(); - }); -}); - -test.describe('Item', () => { - - test('should allow me to mark items as complete', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - // Create two items. - for (const item of TODO_ITEMS.slice(0, 2)) { - await newTodo.fill(item); - await newTodo.press('Enter'); - } - - // Check first item. - const firstTodo = page.getByTestId('todo-item').nth(0); - await firstTodo.getByRole('checkbox').check(); - await expect(firstTodo).toHaveClass('completed'); - - // Check second item. - const secondTodo = page.getByTestId('todo-item').nth(1); - await expect(secondTodo).not.toHaveClass('completed'); - await secondTodo.getByRole('checkbox').check(); - - // Assert completed class. - await expect(firstTodo).toHaveClass('completed'); - await expect(secondTodo).toHaveClass('completed'); - }); - - test('should allow me to un-mark items as complete', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - // Create two items. - for (const item of TODO_ITEMS.slice(0, 2)) { - await newTodo.fill(item); - await newTodo.press('Enter'); - } - - const firstTodo = page.getByTestId('todo-item').nth(0); - const secondTodo = page.getByTestId('todo-item').nth(1); - const firstTodoCheckbox = firstTodo.getByRole('checkbox'); - - await firstTodoCheckbox.check(); - await expect(firstTodo).toHaveClass('completed'); - await expect(secondTodo).not.toHaveClass('completed'); - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - - await firstTodoCheckbox.uncheck(); - await expect(firstTodo).not.toHaveClass('completed'); - await expect(secondTodo).not.toHaveClass('completed'); - await checkNumberOfCompletedTodosInLocalStorage(page, 0); - }); - - test('should allow me to edit an item', async ({ page }) => { - await createDefaultTodos(page); - - const todoItems = page.getByTestId('todo-item'); - const secondTodo = todoItems.nth(1); - await secondTodo.dblclick(); - await expect(secondTodo.getByRole('textbox', { name: 'Edit' })).toHaveValue(TODO_ITEMS[1]); - await secondTodo.getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); - await secondTodo.getByRole('textbox', { name: 'Edit' }).press('Enter'); - - // Explicitly assert the new text value. - await expect(todoItems).toHaveText([ - TODO_ITEMS[0], - 'buy some sausages', - TODO_ITEMS[2] - ]); - await checkTodosInLocalStorage(page, 'buy some sausages'); - }); -}); - -test.describe('Editing', () => { - test.beforeEach(async ({ page }) => { - await createDefaultTodos(page); - await checkNumberOfTodosInLocalStorage(page, 3); - }); - - test('should hide other controls when editing', async ({ page }) => { - const todoItem = page.getByTestId('todo-item').nth(1); - await todoItem.dblclick(); - await expect(todoItem.getByRole('checkbox')).not.toBeVisible(); - await expect(todoItem.locator('label', { - hasText: TODO_ITEMS[1], - })).not.toBeVisible(); - await checkNumberOfTodosInLocalStorage(page, 3); - }); - - test('should save edits on blur', async ({ page }) => { - const todoItems = page.getByTestId('todo-item'); - await todoItems.nth(1).dblclick(); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).dispatchEvent('blur'); - - await expect(todoItems).toHaveText([ - TODO_ITEMS[0], - 'buy some sausages', - TODO_ITEMS[2], - ]); - await checkTodosInLocalStorage(page, 'buy some sausages'); - }); - - test('should trim entered text', async ({ page }) => { - const todoItems = page.getByTestId('todo-item'); - await todoItems.nth(1).dblclick(); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(' buy some sausages '); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter'); - - await expect(todoItems).toHaveText([ - TODO_ITEMS[0], - 'buy some sausages', - TODO_ITEMS[2], - ]); - await checkTodosInLocalStorage(page, 'buy some sausages'); - }); - - test('should remove the item if an empty text string was entered', async ({ page }) => { - const todoItems = page.getByTestId('todo-item'); - await todoItems.nth(1).dblclick(); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(''); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter'); - - await expect(todoItems).toHaveText([ - TODO_ITEMS[0], - TODO_ITEMS[2], - ]); - }); - - test('should cancel edits on escape', async ({ page }) => { - const todoItems = page.getByTestId('todo-item'); - await todoItems.nth(1).dblclick(); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Escape'); - await expect(todoItems).toHaveText(TODO_ITEMS); - }); -}); - -test.describe('Counter', () => { - test('should display the current number of todo items', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - // create a todo count locator - const todoCount = page.getByTestId('todo-count') - - await newTodo.fill(TODO_ITEMS[0]); - await newTodo.press('Enter'); - - await expect(todoCount).toContainText('1'); - - await newTodo.fill(TODO_ITEMS[1]); - await newTodo.press('Enter'); - await expect(todoCount).toContainText('2'); - - await checkNumberOfTodosInLocalStorage(page, 2); - }); -}); - -test.describe('Clear completed button', () => { - test.beforeEach(async ({ page }) => { - await createDefaultTodos(page); - }); - - test('should display the correct text', async ({ page }) => { - await page.locator('.todo-list li .toggle').first().check(); - await expect(page.getByRole('button', { name: 'Clear completed' })).toBeVisible(); - }); - - test('should remove completed items when clicked', async ({ page }) => { - const todoItems = page.getByTestId('todo-item'); - await todoItems.nth(1).getByRole('checkbox').check(); - await page.getByRole('button', { name: 'Clear completed' }).click(); - await expect(todoItems).toHaveCount(2); - await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); - }); - - test('should be hidden when there are no items that are completed', async ({ page }) => { - await page.locator('.todo-list li .toggle').first().check(); - await page.getByRole('button', { name: 'Clear completed' }).click(); - await expect(page.getByRole('button', { name: 'Clear completed' })).toBeHidden(); - }); -}); - -test.describe('Persistence', () => { - test('should persist its data', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - for (const item of TODO_ITEMS.slice(0, 2)) { - await newTodo.fill(item); - await newTodo.press('Enter'); - } - - const todoItems = page.getByTestId('todo-item'); - const firstTodoCheck = todoItems.nth(0).getByRole('checkbox'); - await firstTodoCheck.check(); - await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); - await expect(firstTodoCheck).toBeChecked(); - await expect(todoItems).toHaveClass(['completed', '']); - - // Ensure there is 1 completed item. - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - - // Now reload. - await page.reload(); - await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); - await expect(firstTodoCheck).toBeChecked(); - await expect(todoItems).toHaveClass(['completed', '']); - }); -}); - -test.describe('Routing', () => { - test.beforeEach(async ({ page }) => { - await createDefaultTodos(page); - // make sure the app had a chance to save updated todos in storage - // before navigating to a new view, otherwise the items can get lost :( - // in some frameworks like Durandal - await checkTodosInLocalStorage(page, TODO_ITEMS[0]); - }); - - test('should allow me to display active items', async ({ page }) => { - const todoItem = page.getByTestId('todo-item'); - await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); - - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - await page.getByRole('link', { name: 'Active' }).click(); - await expect(todoItem).toHaveCount(2); - await expect(todoItem).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); - }); - - test('should respect the back button', async ({ page }) => { - const todoItem = page.getByTestId('todo-item'); - await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); - - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - - await test.step('Showing all items', async () => { - await page.getByRole('link', { name: 'All' }).click(); - await expect(todoItem).toHaveCount(3); - }); - - await test.step('Showing active items', async () => { - await page.getByRole('link', { name: 'Active' }).click(); - }); - - await test.step('Showing completed items', async () => { - await page.getByRole('link', { name: 'Completed' }).click(); - }); - - await expect(todoItem).toHaveCount(1); - await page.goBack(); - await expect(todoItem).toHaveCount(2); - await page.goBack(); - await expect(todoItem).toHaveCount(3); - }); - - test('should allow me to display completed items', async ({ page }) => { - await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - await page.getByRole('link', { name: 'Completed' }).click(); - await expect(page.getByTestId('todo-item')).toHaveCount(1); - }); - - test('should allow me to display all items', async ({ page }) => { - await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - await page.getByRole('link', { name: 'Active' }).click(); - await page.getByRole('link', { name: 'Completed' }).click(); - await page.getByRole('link', { name: 'All' }).click(); - await expect(page.getByTestId('todo-item')).toHaveCount(3); - }); - - test('should highlight the currently applied filter', async ({ page }) => { - await expect(page.getByRole('link', { name: 'All' })).toHaveClass('selected'); - - //create locators for active and completed links - const activeLink = page.getByRole('link', { name: 'Active' }); - const completedLink = page.getByRole('link', { name: 'Completed' }); - await activeLink.click(); - - // Page change - active items. - await expect(activeLink).toHaveClass('selected'); - await completedLink.click(); - - // Page change - completed items. - await expect(completedLink).toHaveClass('selected'); - }); -}); - -async function createDefaultTodos(page: Page) { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - for (const item of TODO_ITEMS) { - await newTodo.fill(item); - await newTodo.press('Enter'); - } -} - -async function checkNumberOfTodosInLocalStorage(page: Page, expected: number) { - return await page.waitForFunction(e => { - return JSON.parse(localStorage['react-todos']).length === e; - }, expected); -} - -async function checkNumberOfCompletedTodosInLocalStorage(page: Page, expected: number) { - return await page.waitForFunction(e => { - return JSON.parse(localStorage['react-todos']).filter((todo: any) => todo.completed).length === e; - }, expected); -} - -async function checkTodosInLocalStorage(page: Page, title: string) { - return await page.waitForFunction(t => { - return JSON.parse(localStorage['react-todos']).map((todo: any) => todo.title).includes(t); - }, title); -} From 1d0f086a0b1ec17c58ca744ad60d720b7f03e971 Mon Sep 17 00:00:00 2001 From: Will Nilges Date: Sun, 4 May 2025 14:03:50 -0400 Subject: [PATCH 05/12] checkpoint: I am on the train now --- frontend/cluesheet-web/package-lock.json | 623 ++++++++++++++++- frontend/cluesheet-web/package.json | 2 + .../src/app/cluesheet/[cluesheet_id]/page.tsx | 7 + frontend/cluesheet-web/src/lib/endpoint.ts | 2 + .../tests/00_cluesheet_viewer.spec.ts | 10 + .../tests/00_join_form_meshdb_500.spec.ts | 74 -- .../tests/01_join_form_meshdb_down.spec.ts | 77 --- .../cluesheet-web/tests/02_join_form.spec.ts | 630 ------------------ .../tests/03_nn_assign_form.spec.ts | 79 --- .../tests/04_join_form_s3_down.spec.ts | 30 - .../tests/05_join_form_i18n.spec.ts | 45 -- frontend/cluesheet-web/tests/lib/data.ts | 1 + frontend/cluesheet-web/tests/mock/handlers.ts | 170 +---- .../tests/mock_meshdb_500/handlers.ts | 21 - .../tests/mock_meshdb_500/test.ts | 16 - frontend/cluesheet-web/tests/util.ts | 185 ----- 16 files changed, 640 insertions(+), 1332 deletions(-) create mode 100644 frontend/cluesheet-web/src/lib/endpoint.ts create mode 100644 frontend/cluesheet-web/tests/00_cluesheet_viewer.spec.ts delete mode 100644 frontend/cluesheet-web/tests/00_join_form_meshdb_500.spec.ts delete mode 100644 frontend/cluesheet-web/tests/01_join_form_meshdb_down.spec.ts delete mode 100644 frontend/cluesheet-web/tests/02_join_form.spec.ts delete mode 100644 frontend/cluesheet-web/tests/03_nn_assign_form.spec.ts delete mode 100644 frontend/cluesheet-web/tests/04_join_form_s3_down.spec.ts delete mode 100644 frontend/cluesheet-web/tests/05_join_form_i18n.spec.ts create mode 100644 frontend/cluesheet-web/tests/lib/data.ts delete mode 100644 frontend/cluesheet-web/tests/mock_meshdb_500/handlers.ts delete mode 100644 frontend/cluesheet-web/tests/mock_meshdb_500/test.ts delete mode 100644 frontend/cluesheet-web/tests/util.ts diff --git a/frontend/cluesheet-web/package-lock.json b/frontend/cluesheet-web/package-lock.json index af2362e..42efdfe 100644 --- a/frontend/cluesheet-web/package-lock.json +++ b/frontend/cluesheet-web/package-lock.json @@ -10,7 +10,9 @@ "dependencies": { "@axa-fr/react-oidc": "^7.25.12", "csh-material-bootstrap": "^4.6.2", + "msw": "^2.7.5", "next": "15.3.1", + "playwright-msw": "^3.0.1", "powerhooks": "^2.0.1", "react": "^19.0.0", "react-dom": "^19.0.0", @@ -66,6 +68,43 @@ "node": ">=6.9.0" } }, + "node_modules/@bundled-es-modules/cookie": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/cookie/-/cookie-2.0.1.tgz", + "integrity": "sha512-8o+5fRPLNbjbdGRRmJj3h6Hh1AQJf2dk3qQ/5ZFb+PXkRNiSoMGGUKlsgLfrxneb72axVJyIYji64E2+nNfYyw==", + "license": "ISC", + "dependencies": { + "cookie": "^0.7.2" + } + }, + "node_modules/@bundled-es-modules/cookie/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@bundled-es-modules/statuses": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/statuses/-/statuses-1.0.1.tgz", + "integrity": "sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg==", + "license": "ISC", + "dependencies": { + "statuses": "^2.0.1" + } + }, + "node_modules/@bundled-es-modules/tough-cookie": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/tough-cookie/-/tough-cookie-0.1.6.tgz", + "integrity": "sha512-dvMHbL464C0zI+Yqxbz6kZ5TOEp7GLW+pry/RWndAR8MJQAXZ2rPmIs8tziTZjeIyhSNZgZbCePtfSbdWqStJw==", + "license": "ISC", + "dependencies": { + "@types/tough-cookie": "^4.0.5", + "tough-cookie": "^4.1.4" + } + }, "node_modules/@emnapi/runtime": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz", @@ -453,6 +492,106 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@inquirer/confirm": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.9.tgz", + "integrity": "sha512-NgQCnHqFTjF7Ys2fsqK2WtnA8X1kHyInyG+nMIuHowVTIgIuS10T4AznI/PvbqSpJqjCUqNBlKGh1v3bwLFL4w==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.10", + "@inquirer/type": "^3.0.6" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "10.1.10", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.10.tgz", + "integrity": "sha512-roDaKeY1PYY0aCqhRmXihrHjoSW2A00pV3Ke5fTpMCkzcGF64R8e0lw3dK+eLEHwS4vB5RnW1wuQmvzoRul8Mw==", + "license": "MIT", + "dependencies": { + "@inquirer/figures": "^1.0.11", + "@inquirer/type": "^3.0.6", + "ansi-escapes": "^4.3.2", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.11.tgz", + "integrity": "sha512-eOg92lvrn/aRUqbxRyvpEWnrvRuTYRifixHkYVpJiygTgVSBIHDqLh0SrMQXkafvULg3ck11V7xvR+zcgvpHFw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.6.tgz", + "integrity": "sha512-/mKVCtVpyBu3IDarv0G+59KC4stsD5mDsGpYh+GKs1NZT88Jh52+cuoA1AtLk2Q0r/quNl+1cSUyLRHBFeD0XA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@mswjs/cookies": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@mswjs/cookies/-/cookies-1.1.1.tgz", + "integrity": "sha512-W68qOHEjx1iD+4VjQudlx26CPIoxmIAtK4ZCexU0/UJBG6jYhcuyzKJx+Iw8uhBIGd9eba64XgWVgo20it1qwA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@mswjs/interceptors": { + "version": "0.37.6", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.37.6.tgz", + "integrity": "sha512-wK+5pLK5XFmgtH3aQ2YVvA3HohS3xqV/OxuVOdNx9Wpnz7VE/fnC+e1A7ln6LFYeck7gOJ/dsZV6OLplOtAJ2w==", + "license": "MIT", + "dependencies": { + "@open-draft/deferred-promise": "^2.2.0", + "@open-draft/logger": "^0.3.0", + "@open-draft/until": "^2.0.0", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "strict-event-emitter": "^0.5.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@next/env": { "version": "15.3.1", "resolved": "https://registry.npmjs.org/@next/env/-/env-15.3.1.tgz", @@ -587,6 +726,28 @@ "node": ">= 10" } }, + "node_modules/@open-draft/deferred-promise": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", + "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==", + "license": "MIT" + }, + "node_modules/@open-draft/logger": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz", + "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", + "license": "MIT", + "dependencies": { + "is-node-process": "^1.2.0", + "outvariant": "^1.4.0" + } + }, + "node_modules/@open-draft/until": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", + "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", + "license": "MIT" + }, "node_modules/@parcel/watcher": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", @@ -915,7 +1076,6 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.52.0.tgz", "integrity": "sha512-uh6W7sb55hl7D6vsAeA+V2p5JnlAqzhqFyF0VcJkKZXkgnFcVG9PziERRHQfPLfNGx1C292a4JqbWzhR8L4R1g==", - "devOptional": true, "license": "Apache-2.0", "dependencies": { "playwright": "1.52.0" @@ -952,11 +1112,17 @@ "tslib": "^2.8.0" } }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "license": "MIT" + }, "node_modules/@types/node": { "version": "20.17.32", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.32.tgz", "integrity": "sha512-zeMXFn8zQ+UkjK4ws0RiOC9EWByyW1CcVmLe+2rQocXRsGEDxUCwPEIVgpsGcLHS/P8JkT0oa3839BRABS0oPw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "undici-types": "~6.19.2" @@ -982,6 +1148,69 @@ "@types/react": "^19.0.0" } }, + "node_modules/@types/statuses": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.5.tgz", + "integrity": "sha512-jmIUGWrAiwu3dZpxntxieC+1n/5c3mjrImkmOSQ2NC5uP6cYO4aAZDdSmRcI5C1oiTmqlZGHC+/NmJrKogbP5A==", + "license": "MIT" + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "license": "MIT" + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/braces": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", @@ -1049,12 +1278,52 @@ "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", "license": "MIT" }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "license": "MIT" }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/color": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", @@ -1074,7 +1343,6 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", - "optional": true, "dependencies": { "color-name": "~1.1.4" }, @@ -1086,8 +1354,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/color-string": { "version": "1.9.1", @@ -1157,6 +1424,12 @@ "csstype": "^3.0.2" } }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, "node_modules/es5-ext": { "version": "0.10.64", "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", @@ -1209,6 +1482,15 @@ "es6-symbol": "^3.1.1" } }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/esniff": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", @@ -1272,7 +1554,6 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -1283,6 +1564,30 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/graphql": { + "version": "16.11.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.11.0.tgz", + "integrity": "sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==", + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, + "node_modules/headers-polyfill": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.3.tgz", + "integrity": "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==", + "license": "MIT" + }, "node_modules/immutable": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.1.tgz", @@ -1308,6 +1613,15 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -1322,6 +1636,12 @@ "node": ">=0.10.0" } }, + "node_modules/is-node-process": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", + "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", + "license": "MIT" + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -1406,6 +1726,59 @@ "integrity": "sha512-oxdmJ9cL+xV72h0xYxp4tP2d5/fTBpP45H8DIOn9pASuF8a3IYTf+25fMGDYGiWW+MFsuog6KD6nfmhZJQ+uUw==", "license": "MIT" }, + "node_modules/msw": { + "version": "2.7.5", + "resolved": "https://registry.npmjs.org/msw/-/msw-2.7.5.tgz", + "integrity": "sha512-00MyTlY3TJutBa5kiU+jWiz2z5pNJDYHn2TgPkGkh92kMmNH43RqvMXd8y/7HxNn8RjzUbvZWYZjcS36fdb6sw==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@bundled-es-modules/cookie": "^2.0.1", + "@bundled-es-modules/statuses": "^1.0.1", + "@bundled-es-modules/tough-cookie": "^0.1.6", + "@inquirer/confirm": "^5.0.0", + "@mswjs/interceptors": "^0.37.0", + "@open-draft/deferred-promise": "^2.2.0", + "@open-draft/until": "^2.1.0", + "@types/cookie": "^0.6.0", + "@types/statuses": "^2.0.4", + "graphql": "^16.8.1", + "headers-polyfill": "^4.0.2", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "path-to-regexp": "^6.3.0", + "picocolors": "^1.1.1", + "strict-event-emitter": "^0.5.1", + "type-fest": "^4.26.1", + "yargs": "^17.7.2" + }, + "bin": { + "msw": "cli/index.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mswjs" + }, + "peerDependencies": { + "typescript": ">= 4.8.x" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -1501,6 +1874,18 @@ "node": ">=0.10.0" } }, + "node_modules/outvariant": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", + "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==", + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -1525,7 +1910,6 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.52.0.tgz", "integrity": "sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==", - "devOptional": true, "license": "Apache-2.0", "dependencies": { "playwright-core": "1.52.0" @@ -1544,7 +1928,6 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.52.0.tgz", "integrity": "sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==", - "devOptional": true, "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" @@ -1553,6 +1936,23 @@ "node": ">=18" } }, + "node_modules/playwright-msw": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/playwright-msw/-/playwright-msw-3.0.1.tgz", + "integrity": "sha512-w2bVjt7kPIThOQF9OS/1vDDs0HsQfV9inxMVSUv74x/zhCcrgzVN47xpPk84okf3OcCRHHBJKq8sNeBfCDyhMg==", + "license": "MIT", + "dependencies": { + "@mswjs/cookies": "^1.1.0", + "strict-event-emitter": "^0.5.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@playwright/test": ">=1.20.0", + "msw": "^2.0.0" + } + }, "node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -1619,6 +2019,33 @@ "react-is": "^16.13.1" } }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "license": "MIT" + }, "node_modules/react": { "version": "19.1.0", "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", @@ -1754,6 +2181,21 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "license": "MIT" + }, "node_modules/run-exclusive": { "version": "2.2.19", "resolved": "https://registry.npmjs.org/run-exclusive/-/run-exclusive-2.2.19.tgz", @@ -1850,6 +2292,18 @@ "@img/sharp-win32-x64": "0.34.1" } }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/simple-swizzle": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", @@ -1869,6 +2323,15 @@ "node": ">=0.10.0" } }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/streamsearch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", @@ -1877,6 +2340,38 @@ "node": ">=10.0.0" } }, + "node_modules/strict-event-emitter": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", + "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==", + "license": "MIT" + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/styled-jsx": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", @@ -1927,6 +2422,21 @@ "node": ">=8.0" } }, + "node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/tsafe": { "version": "1.8.5", "resolved": "https://registry.npmjs.org/tsafe/-/tsafe-1.8.5.tgz", @@ -1951,11 +2461,23 @@ "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==", "license": "ISC" }, + "node_modules/type-fest": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.40.1.tgz", + "integrity": "sha512-9YvLNnORDpI+vghLU/Nf+zSv0kL47KbVJ1o3sKgoTefl6i+zebxbiDQWoe/oWWqPhIgQdRZRT1KA9sCPL810SA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/typescript": { "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -1969,9 +2491,28 @@ "version": "6.19.8", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", - "dev": true, + "devOptional": true, "license": "MIT" }, + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "node_modules/warning": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", @@ -1980,6 +2521,68 @@ "dependencies": { "loose-envify": "^1.0.0" } + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz", + "integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/frontend/cluesheet-web/package.json b/frontend/cluesheet-web/package.json index f1f4638..b78f5c9 100644 --- a/frontend/cluesheet-web/package.json +++ b/frontend/cluesheet-web/package.json @@ -11,7 +11,9 @@ "dependencies": { "@axa-fr/react-oidc": "^7.25.12", "csh-material-bootstrap": "^4.6.2", + "msw": "^2.7.5", "next": "15.3.1", + "playwright-msw": "^3.0.1", "powerhooks": "^2.0.1", "react": "^19.0.0", "react-dom": "^19.0.0", diff --git a/frontend/cluesheet-web/src/app/cluesheet/[cluesheet_id]/page.tsx b/frontend/cluesheet-web/src/app/cluesheet/[cluesheet_id]/page.tsx index ea16e23..767c6b6 100644 --- a/frontend/cluesheet-web/src/app/cluesheet/[cluesheet_id]/page.tsx +++ b/frontend/cluesheet-web/src/app/cluesheet/[cluesheet_id]/page.tsx @@ -1,3 +1,5 @@ +import { cluesheetBackendEndpoint } from "@/lib/endpoint"; + export const metadata = { title: "View cluesheet", description: "View an mf cluesheet kerchoo", @@ -9,6 +11,11 @@ export default async function CluesheetViewer({ params: Promise<{ cluesheet_id: string }>; }) { const { cluesheet_id } = await params; + + const cluesheet = await fetch(`${cluesheetBackendEndpoint}/${cluesheet_id}`); + + console.log(`Got cluesheet object: ${await cluesheet.json()}`); + return ( <>
diff --git a/frontend/cluesheet-web/src/lib/endpoint.ts b/frontend/cluesheet-web/src/lib/endpoint.ts new file mode 100644 index 0000000..00d6104 --- /dev/null +++ b/frontend/cluesheet-web/src/lib/endpoint.ts @@ -0,0 +1,2 @@ +export const cluesheetBackendEndpoint = process.env.NEXT_PUBLIC_CLUESHEET_BACKEND_ENDPOINT ?? ""; + diff --git a/frontend/cluesheet-web/tests/00_cluesheet_viewer.spec.ts b/frontend/cluesheet-web/tests/00_cluesheet_viewer.spec.ts new file mode 100644 index 0000000..2768461 --- /dev/null +++ b/frontend/cluesheet-web/tests/00_cluesheet_viewer.spec.ts @@ -0,0 +1,10 @@ +import { mockCluesheetUUID } from "./lib/data"; +import { test, expect } from "./mock/test"; + +const unitTestTimeout = 10000; + +test("happy view cluesheet", async ({ page }) => { + test.setTimeout(unitTestTimeout); + await page.goto(`/cluesheet/${mockCluesheetUUID}`); + await page.waitForTimeout(5000); +}); diff --git a/frontend/cluesheet-web/tests/00_join_form_meshdb_500.spec.ts b/frontend/cluesheet-web/tests/00_join_form_meshdb_500.spec.ts deleted file mode 100644 index 0d33bb7..0000000 --- a/frontend/cluesheet-web/tests/00_join_form_meshdb_500.spec.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { test, expect } from "./mock_meshdb_500/test"; -import { - fillOutJoinForm, - sampleData, - sampleJoinRecord, - submitSuccessExpected, -} from "./util"; -import { JoinRecord } from "@/lib/types"; -import { getJoinRecordFromS3 } from "@/lib/join_record"; -import { isDeepStrictEqual } from "util"; - -const joinFormTimeout = 20000; -const unitTestTimeout = 5000; - -const meshdbIsDownText = - "You will receive an email from us in the next 2-3 days with next steps, including how to submit panorama photos."; - -test("meshdb is 500ing but succeed anyway", async ({ page }) => { - test.setTimeout(joinFormTimeout); - await page.goto("/join"); - - // Is the page title correct? - await expect(page).toHaveTitle(/Join Our Community Network!/); - - // Set up sample data. - await fillOutJoinForm(page, sampleData); - - // Uncomment this if you want to poke around after the join form has been filled out - //await page.pause(); - - await submitSuccessExpected(page, unitTestTimeout); - - await expect(page.locator("[id='p-thank-you-01']")).toHaveText( - meshdbIsDownText, - ); - - const joinRecordKey = await page.getAttribute( - '[data-testid="test-join-record-key"]', - "data-state", - ); - - if (joinRecordKey === null) { - throw new Error("Got null join record"); - } - - // This is wacky because playwright has to run this and therefore - // needs our dotenv. - const joinRecord: JoinRecord = await getJoinRecordFromS3(joinRecordKey); - - // Crappy hack - joinRecord.submission_time = sampleJoinRecord.submission_time; - joinRecord.uuid = sampleJoinRecord.uuid; - - // In this case, we know that we won't have a code or install number, so drop - // those from the comparison - let hardDownSampleJoinRecord = structuredClone(sampleJoinRecord); - hardDownSampleJoinRecord.code = 500; - hardDownSampleJoinRecord.install_number = null; - - if (!isDeepStrictEqual(joinRecord, hardDownSampleJoinRecord)) { - console.error("Expected:"); - console.error(hardDownSampleJoinRecord); - console.error("Got:"); - console.error(joinRecord); - throw new Error("Bad JoinRecord. JoinRecord is not deeply equal."); - } - - // Then go home - await page.waitForTimeout(1000); - await page.locator("[name='home']").click(); - await page.waitForTimeout(1000); - const currentURL = new URL(page.url()); - expect(currentURL.pathname).toBe("/"); -}); diff --git a/frontend/cluesheet-web/tests/01_join_form_meshdb_down.spec.ts b/frontend/cluesheet-web/tests/01_join_form_meshdb_down.spec.ts deleted file mode 100644 index c151f49..0000000 --- a/frontend/cluesheet-web/tests/01_join_form_meshdb_down.spec.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { test, expect } from "@playwright/test"; -import { - fillOutJoinForm, - sampleData, - sampleJoinRecord, - submitFailureExpected, - submitSuccessExpected, -} from "./util"; -import { JoinRecord } from "@/lib/types"; -import { getJoinRecordFromS3 } from "@/lib/join_record"; -import { isDeepStrictEqual } from "util"; - -const joinFormTimeout = 20000; -const unitTestTimeout = 5000; - -const meshdbIsDownText = - "You will receive an email from us in the next 2-3 days with next steps, including how to submit panorama photos."; - -test("meshdb is hard down but succeed anyway", async ({ page }) => { - // Block access to the join form API - await page.route("**/api/v1/join/**", (route) => route.abort()); - - test.setTimeout(joinFormTimeout); - await page.goto("/join"); - - // Is the page title correct? - await expect(page).toHaveTitle(/Join Our Community Network!/); - - // Set up sample data. - await fillOutJoinForm(page, sampleData); - - // Uncomment this if you want to poke around after the join form has been filled out - //await page.pause(); - - await submitSuccessExpected(page, unitTestTimeout); - - await expect(page.locator("[id='p-thank-you-01']")).toHaveText( - meshdbIsDownText, - ); - - const joinRecordKey = await page.getAttribute( - '[data-testid="test-join-record-key"]', - "data-state", - ); - - if (joinRecordKey === null) { - throw new Error("Got null join record"); - } - - // This is wacky because playwright has to run this and therefore - // needs our dotenv. - const joinRecord: JoinRecord = await getJoinRecordFromS3(joinRecordKey); - - joinRecord.submission_time = sampleJoinRecord.submission_time; - joinRecord.uuid = sampleJoinRecord.uuid; - - // In this case, we know that we won't have a code or install number, so drop - // those from the comparison - let hardDownSampleJoinRecord = structuredClone(sampleJoinRecord); - hardDownSampleJoinRecord.code = null; - hardDownSampleJoinRecord.install_number = null; - - if (!isDeepStrictEqual(joinRecord, hardDownSampleJoinRecord)) { - console.error("Expected:"); - console.error(hardDownSampleJoinRecord); - console.error("Got:"); - console.error(joinRecord); - throw new Error("Bad JoinRecord. JoinRecord is not deeply equal."); - } - - // Then go home - await page.waitForTimeout(1000); - await page.locator("[name='home']").click(); - await page.waitForTimeout(1000); - const currentURL = new URL(page.url()); - expect(currentURL.pathname).toBe("/"); -}); diff --git a/frontend/cluesheet-web/tests/02_join_form.spec.ts b/frontend/cluesheet-web/tests/02_join_form.spec.ts deleted file mode 100644 index 4982160..0000000 --- a/frontend/cluesheet-web/tests/02_join_form.spec.ts +++ /dev/null @@ -1,630 +0,0 @@ -import { getJoinRecordFromS3 } from "@/lib/join_record"; -import { JoinRecord } from "@/lib/types"; -import { JoinFormValues } from "@/components/JoinForm/JoinForm"; -import { test, expect } from "./mock/test"; - -import { - sampleData, - fillOutJoinForm, - submitSuccessExpected, - submitFailureExpected, - submitConfirmationDialogExpected, - sampleNJData, - submitAndCheckToast, - expectSuccess, - sampleJoinRecord, - findJoinRecord, - chomSt, -} from "./util"; -import { isDeepStrictEqual } from "util"; - -const joinFormTimeout = 20000; -const unitTestTimeout = 5000; - -// Unit tests for the Join Form. -// -// These tests will mock a connection to MeshDB. It is simply making sure that -// the form creates a good-looking payload and can hit a mock API. - -test("change language from english to spanish", async ({ page }) => { - test.setTimeout(20000); - await page.goto("/join"); - - // Is the page title correct? - await expect(page).toHaveTitle(/Join Our Community Network!/); - - await page - .locator("[id='joinform-locale-switcher-select']") - .selectOption("🇪🇸 Español"); - - // TODO (wdn): It would be nice if we could grab the translation out of the - // json blob instead of hardcoding it here. - await expect(page.locator("[id='joinform-title']")).toHaveText( - "Únase NYC Mesh", - ); - - // Set up sample data. - await fillOutJoinForm(page, sampleData); - - await submitSuccessExpected(page, unitTestTimeout); - await expect(page.locator("[id='alert-thank-you-h2']")).toHaveText( - "¡Gracias! Por favor revisa su correo electronico.", - ); -}); - -test("happy join form", async ({ page }) => { - test.setTimeout(joinFormTimeout); - await page.goto("/join"); - - // Is the page title correct? - await expect(page).toHaveTitle(/Join Our Community Network!/); - - // Set up sample data. - await fillOutJoinForm(page, sampleData); - - // Uncomment this if you want to poke around after the join form has been filled out - //await page.pause(); - - await submitSuccessExpected(page, unitTestTimeout); - - // Hardcoding the translation messages to ensure that translation is actually working. - // If it breaks, it'll display the message key instead. - const thankYouTitle = "Thanks! Please check your email."; - const thankYouText = - "You will receive an email from us in the next 5-10 minutes with next steps, including how to submit panorama photos."; - - await expect(page.locator("[id='alert-thank-you-h2']")).toHaveText( - thankYouTitle, - ); - await expect(page.locator("[id='p-thank-you-01']")).toHaveText(thankYouText); - - const joinRecordKey = await page.getAttribute( - '[data-testid="test-join-record-key"]', - "data-state", - ); - - if (joinRecordKey === null) { - throw new Error("Got null join record"); - } - - // This is wacky because playwright has to run this and therefore - // needs our dotenv. - const joinRecord: JoinRecord = await getJoinRecordFromS3(joinRecordKey); - - // This is fucked, sorry. - joinRecord.submission_time = sampleJoinRecord.submission_time; - joinRecord.uuid = sampleJoinRecord.uuid; - - if (!isDeepStrictEqual(joinRecord, sampleJoinRecord)) { - console.error("Expected:"); - console.error(sampleJoinRecord); - console.error("Got:"); - console.error(joinRecord); - throw new Error("Bad JoinRecord. JoinRecord is not deeply equal."); - } - - // Then go home - await page.waitForTimeout(1000); - await page.locator("[name='home']").click(); - await page.waitForTimeout(1000); - const currentURL = new URL(page.url()); - expect(currentURL.pathname).toBe("/"); -}); - -// Tests missing phone -test("happy join form missing phone", async ({ page }) => { - test.setTimeout(joinFormTimeout); - await page.goto("/join"); - - // Is the page title correct? - await expect(page).toHaveTitle(/Join Our Community Network!/); - - let missingData: JoinFormValues = Object.assign({}, sampleData); - missingData.phone_number = ""; - - // Set up sample data. - await fillOutJoinForm(page, missingData); - await submitSuccessExpected(page, unitTestTimeout); -}); - -test("confirm city", async ({ page }) => { - test.setTimeout(joinFormTimeout); - await page.goto("/join"); - - // Is the page title correct? - await expect(page).toHaveTitle(/Join Our Community Network!/); - let data: JoinFormValues = Object.assign({}, sampleData); - data.city = "brooklyn"; - - // Set up sample data. - await fillOutJoinForm(page, data); - - // Uncomment this if you want to poke around after the join form has been filled out - //await page.pause(); - - await submitConfirmationDialogExpected(page, 2000); - - // Check that the Join Record's code is correct. - let joinRecord = await findJoinRecord(page); - let code = 409; - if (joinRecord.code !== code) { - throw new Error( - `JoinRecord code (${joinRecord.code}) did not match expected code (${code})`, - ); - } - - await page.locator("[name='confirm']").click(); - - // Make sure the JoinRecord updated properly - await page.waitForTimeout(1000); - joinRecord = await findJoinRecord(page); - if (joinRecord.code !== 201) { - throw new Error( - `JoinRecord code (${joinRecord.code}) did not match expected code (201)`, - ); - } - - await expectSuccess(page, 1000); -}); - -test("confirm street address", async ({ page }) => { - test.setTimeout(joinFormTimeout); - await page.goto("/join"); - - // Is the page title correct? - await expect(page).toHaveTitle(/Join Our Community Network!/); - let data = structuredClone(sampleData); - data.street_address = "197 prospect pl"; - - // Set up sample data. - await fillOutJoinForm(page, data); - - // Uncomment this if you want to poke around after the join form has been filled out - //await page.pause(); - - await submitConfirmationDialogExpected(page, 2000); - - await page.locator("[name='confirm']").click(); - - await expectSuccess(page, unitTestTimeout); -}); - -test("street address trust me bro", async ({ page }) => { - test.setTimeout(joinFormTimeout); - await page.goto("/join"); - - // Is the page title correct? - await expect(page).toHaveTitle(/Join Our Community Network!/); - let data: JoinFormValues = Object.assign({}, sampleData); - data.street_address = chomSt; - - // Set up sample data. - await fillOutJoinForm(page, data); - - // Uncomment this if you want to poke around after the join form has been filled out - //await page.pause(); - - await submitConfirmationDialogExpected(page, 2000); - - await page.locator("[name='reject']").click(); - - await expectSuccess(page, unitTestTimeout); -}); - -// TODO: Add a garbage testcase -// TODO: Add confirm block (is this trust me bro?) - -// Tests missing both first and last name -test("fail missing first name", async ({ page }) => { - test.setTimeout(joinFormTimeout); - await page.goto("/join"); - - // Is the page title correct? - await expect(page).toHaveTitle(/Join Our Community Network!/); - - let missingNameData: JoinFormValues = Object.assign({}, sampleData); - missingNameData.first_name = ""; - - // Set up sample data. - await fillOutJoinForm(page, missingNameData); - - await page.locator("[name='submit_join_form']").click(); - - await expect(page.locator("[name='submit_join_form']")).toBeEnabled(); - await expect(page.locator("[id='error_first_name']")).toBeVisible(); -}); - -test("fail missing last name", async ({ page }) => { - test.setTimeout(joinFormTimeout); - await page.goto("/join"); - - // Is the page title correct? - await expect(page).toHaveTitle(/Join Our Community Network!/); - - // Do it again with last name - let missingNameData: JoinFormValues = Object.assign({}, sampleData); - missingNameData.last_name = ""; - await fillOutJoinForm(page, missingNameData); - - await page.locator("[name='submit_join_form']").click(); - - await expect(page.locator("[name='submit_join_form']")).toBeEnabled(); - await expect(page.locator("[id='error_last_name']")).toBeVisible(); -}); - -// Tests missing email -test("fail missing email", async ({ page }) => { - test.setTimeout(joinFormTimeout); - await page.goto("/join"); - - // Is the page title correct? - await expect(page).toHaveTitle(/Join Our Community Network!/); - - let missingData: JoinFormValues = Object.assign({}, sampleData); - missingData.email_address = ""; - - // Set up sample data. - await fillOutJoinForm(page, missingData); - - await page.locator("[name='submit_join_form']").click(); - - await expect(page.locator("[name='submit_join_form']")).toBeEnabled(); - await expect(page.locator("[id='error_email_address']")).toBeVisible(); -}); - -// Tests missing email + phone -test("fail missing email and phone", async ({ page }) => { - test.setTimeout(joinFormTimeout); - await page.goto("/join"); - - // Is the page title correct? - await expect(page).toHaveTitle(/Join Our Community Network!/); - - let missingData: JoinFormValues = Object.assign({}, sampleData); - missingData.phone_number = ""; - missingData.email_address = ""; - - // Set up sample data. - await fillOutJoinForm(page, missingData); - - await page.locator("[name='submit_join_form']").click(); - - await expect(page.locator("[name='submit_join_form']")).toBeEnabled(); - await expect(page.locator("[id='error_email_address']")).toBeVisible(); - await expect(page.locator("[id='error_phone_number']")).toBeHidden(); -}); - -// Give a bad email address -test("fail bad email", async ({ page }) => { - test.setTimeout(joinFormTimeout); - await page.goto("/join"); - - // Is the page title correct? - await expect(page).toHaveTitle(/Join Our Community Network!/); - - let missingData: JoinFormValues = Object.assign({}, sampleData); - missingData.email_address = "notagoodemail"; - - // Set up sample data. - await fillOutJoinForm(page, missingData); - - await page.locator("[name='submit_join_form']").click(); - - await expect(page.locator("[name='submit_join_form']")).toBeEnabled(); - await expect(page.locator("[id='error_email_address']")).toBeVisible(); -}); - -test("fail bad email 2", async ({ page }) => { - test.setTimeout(joinFormTimeout); - await page.goto("/join"); - - // Is the page title correct? - await expect(page).toHaveTitle(/Join Our Community Network!/); - - let missingData: JoinFormValues = Object.assign({}, sampleData); - - // This email is bad, in way that our basic validation doesn't catch but the backend does - missingData.email_address = "a@b"; - await fillOutJoinForm(page, missingData); - await submitFailureExpected(page); -}); - -// Tests bad phone -test("fail bad phone", async ({ page }) => { - test.setTimeout(joinFormTimeout); - await page.goto("/join"); - - // Is the page title correct? - await expect(page).toHaveTitle(/Join Our Community Network!/); - - let missingData: JoinFormValues = Object.assign({}, sampleData); - missingData.phone_number = "twelve"; - - // Set up sample data. - await fillOutJoinForm(page, missingData); - - await page.locator("[name='submit_join_form']").click(); - - await expect(page.locator("[name='submit_join_form']")).toBeEnabled(); - await expect(page.locator("[id='error_phone_number']")).toBeVisible(); -}); - -test("fail bad phone 2", async ({ page }) => { - test.setTimeout(joinFormTimeout); - await page.goto("/join"); - - // Is the page title correct? - await expect(page).toHaveTitle(/Join Our Community Network!/); - - let missingData: JoinFormValues = Object.assign({}, sampleData); - missingData.phone_number = "12"; - await fillOutJoinForm(page, missingData); - await submitFailureExpected(page); -}); - -test("fail missing address", async ({ page }) => { - test.setTimeout(joinFormTimeout); - await page.goto("/join"); - - // Is the page title correct? - await expect(page).toHaveTitle(/Join Our Community Network!/); - - let missingAddressData: JoinFormValues = Object.assign({}, sampleData); - missingAddressData.street_address = ""; - await fillOutJoinForm(page, missingAddressData); - - await page.locator("[name='submit_join_form']").click(); - - await expect(page.locator("[name='submit_join_form']")).toBeEnabled(); - await expect(page.locator("[id='error_street_address']")).toBeVisible(); -}); - -test("fail missing city", async ({ page }) => { - test.setTimeout(joinFormTimeout); - await page.goto("/join"); - - // Is the page title correct? - await expect(page).toHaveTitle(/Join Our Community Network!/); - - let missingAddressData: JoinFormValues = Object.assign({}, sampleData); - missingAddressData.city = ""; - await fillOutJoinForm(page, missingAddressData); - - await page.locator("[name='submit_join_form']").click(); - - await expect(page.locator("[name='submit_join_form']")).toBeEnabled(); - await expect(page.locator("[id='error_city']")).toBeVisible(); -}); - -// This one should fail and here's why: It's really annoying when people -// don't give us their apartment #, so we make it required at the expense -// of those who live in houses. They can just write "house" or "N/A" or -// something -// TODO (wdn): Add a checkbox for my house-havers -test("fail missing unit number", async ({ page }) => { - test.setTimeout(joinFormTimeout); - await page.goto("/join"); - - // Is the page title correct? - await expect(page).toHaveTitle(/Join Our Community Network!/); - - let missingAddressData: JoinFormValues = Object.assign({}, sampleData); - missingAddressData.apartment = ""; - await fillOutJoinForm(page, missingAddressData); - - await page.locator("[name='submit_join_form']").click(); - - await expect(page.locator("[name='submit_join_form']")).toBeEnabled(); - await expect(page.locator("[id='error_apartment']")).toBeVisible(); -}); - -test("fail nj", async ({ page }) => { - test.setTimeout(joinFormTimeout); - await page.goto("/join"); - - // Is the page title correct? - await expect(page).toHaveTitle(/Join Our Community Network!/); - - // Set up sample data. - await fillOutJoinForm(page, sampleNJData); - - // Uncomment this if you want to poke around after the join form has been filled out - //await page.pause(); - - await submitAndCheckToast( - page, - "Non-NYC registrations are not supported at this time", - ); - - // Check that the Join Record's code is correct. - let joinRecord = await findJoinRecord(page); - let code = 400; - if (joinRecord.code !== code) { - throw new Error( - `JoinRecord code (${joinRecord.code}) did not match expected code (${code})`, - ); - } -}); - -test.describe("user triggered captchaV2", () => { - test.skip(process.env.RUN_CAPTCHA !== "true"); - test("user triggered captchaV2", async ({ page }) => { - test.setTimeout(joinFormTimeout); - await page.goto("/join"); - - // Is the page title correct? - await expect(page).toHaveTitle(/Join Our Community Network!/); - - // Set up sample data. - let botTriggeringData: JoinFormValues = Object.assign({}, sampleData); - - await fillOutJoinForm(page, botTriggeringData); - - await submitAndCheckToast( - page, - "Please complete an additional verification step to confirm your submission", - ); - - await page.waitForTimeout(1000); - - // Make the robot check the "I'm not a robot" button (commit voter fraud) - await page - .locator("[title='reCAPTCHA']") - .nth(1) - .contentFrame() - .locator("[id='recaptcha-anchor']") - .click(); - - await submitSuccessExpected(page, unitTestTimeout); - }); - - test("user triggered captchaV2 and trust me bro", async ({ page }) => { - test.setTimeout(joinFormTimeout * 2); // This is a really long test - await page.goto("/join"); - - // Is the page title correct? - await expect(page).toHaveTitle(/Join Our Community Network!/); - - // Set up sample data. - let botTriggeringData: JoinFormValues = Object.assign({}, sampleData); - botTriggeringData.street_address = chomSt; - - await fillOutJoinForm(page, botTriggeringData); - - await submitAndCheckToast( - page, - "Please complete an additional verification step to confirm your submission", - ); - - await page.waitForTimeout(1000); - - // Make the robot check the "I'm not a robot" button (commit voter fraud) - await page - .locator("[title='reCAPTCHA']") - .nth(1) - .contentFrame() - .locator("[id='recaptcha-anchor']") - .click(); - - await submitConfirmationDialogExpected(page, 2000); - - // 2 counts of voter fraud - await page - .locator("[title='reCAPTCHA']") - .nth(2) - .contentFrame() - .locator("[id='recaptcha-anchor']") - .click(); - - await page.locator("[name='confirm']").click(); - - await expectSuccess(page, unitTestTimeout); - }); - - test("user triggered captchaV2 and trust me bro and reject changes", async ({ - page, - }) => { - test.setTimeout(joinFormTimeout * 2); // This is a really long test - await page.goto("/join"); - - // Is the page title correct? - await expect(page).toHaveTitle(/Join Our Community Network!/); - - // Set up sample data. - let botTriggeringData: JoinFormValues = Object.assign({}, sampleData); - botTriggeringData.street_address = chomSt; - - await fillOutJoinForm(page, botTriggeringData); - - await submitAndCheckToast( - page, - "Please complete an additional verification step to confirm your submission", - ); - - await page.waitForTimeout(1000); - - // Make the robot check the "I'm not a robot" button (commit voter fraud) - await page - .locator("[title='reCAPTCHA']") - .nth(1) - .contentFrame() - .locator("[id='recaptcha-anchor']") - .click(); - - await submitConfirmationDialogExpected(page, 2000); - - // 2 counts of voter fraud - await page - .locator("[title='reCAPTCHA']") - .nth(2) - .contentFrame() - .locator("[id='recaptcha-anchor']") - .click(); - - await page.locator("[name='reject']").click(); - - await expectSuccess(page, unitTestTimeout); - }); - - test("user triggered captchaV2 and trust me bro and cancel and try again", async ({ - page, - }) => { - test.setTimeout(joinFormTimeout * 2); // This is a really long test - await page.goto("/join"); - - // Is the page title correct? - await expect(page).toHaveTitle(/Join Our Community Network!/); - - // Set up sample data. - let botTriggeringData: JoinFormValues = Object.assign({}, sampleData); - botTriggeringData.street_address = chomSt; - - // Fill out the form with our data - await fillOutJoinForm(page, botTriggeringData); - - // Expect a warning asking us to do a captcha - await submitAndCheckToast( - page, - "Please complete an additional verification step to confirm your submission", - ); - - // Make the robot check the "I'm not a robot" button (commit voter fraud) - await page.waitForTimeout(1000); - await page - .locator("[title='reCAPTCHA']") - .nth(1) - .contentFrame() - .locator("[id='recaptcha-anchor']") - .click(); - - // Expect the dialogue to show up - await submitConfirmationDialogExpected(page, 2000); - - // dismiss it - await page.waitForTimeout(1000); - await page.locator("[name='cancel']").click(); - - // Do the captcha again - await page - .locator("[title='reCAPTCHA']") - .nth(1) - .contentFrame() - .locator("[id='recaptcha-anchor']") - .click(); - - // Try submitting again - await submitConfirmationDialogExpected(page, 2000); - - // 3 counts of voter fraud - await page - .locator("[title='reCAPTCHA']") - .nth(2) - .contentFrame() - .locator("[id='recaptcha-anchor']") - .click(); - - await page.locator("[name='confirm']").click(); - - await expectSuccess(page, unitTestTimeout); - }); -}); diff --git a/frontend/cluesheet-web/tests/03_nn_assign_form.spec.ts b/frontend/cluesheet-web/tests/03_nn_assign_form.spec.ts deleted file mode 100644 index bf34fb8..0000000 --- a/frontend/cluesheet-web/tests/03_nn_assign_form.spec.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { test, expect } from "./mock/test"; - -const timeout = 10000; -const clickTimeout = 1000; - -// Unit tests for the Join Form. -// -// These tests will mock a connection to MeshDB. It is simply making sure that -// the form creates a good-looking payload and can hit a mock API. - -test("nn assign happy", async ({ page }) => { - test.setTimeout(timeout); - await page.goto("/nn-assign"); - - await expect(page).toHaveTitle(/Assign a Network Number/); - - await page.getByPlaceholder("Install Number").fill("20000"); - await page.getByPlaceholder("Pre-Shared Key").fill("localdev"); - - await page.getByRole("button", { name: /Submit/i }).click(); - await page.waitForTimeout(clickTimeout); - - await expect(page.locator("[id='alert-network-number']")).toBeVisible(); - await expect(page.locator("[id='assigned-network-number']")).toHaveText( - "420", - ); -}); - -test("nn assign already assigned", async ({ page }) => { - test.setTimeout(timeout); - await page.goto("/nn-assign"); - - await expect(page).toHaveTitle(/Assign a Network Number/); - - await page.getByPlaceholder("Install Number").fill("30000"); - await page.getByPlaceholder("Pre-Shared Key").fill("localdev"); - - await page.getByRole("button", { name: /Submit/i }).click(); - await page.waitForTimeout(clickTimeout); - - await expect(page.locator("[id='alert-network-number']")).toBeVisible(); - await expect(page.locator("[id='nn-message']")).toHaveText( - "This Install Number (30000) already has a Network Number (520) associated with it!", - ); - await expect(page.locator("[id='assigned-network-number']")).toHaveText( - "520", - ); -}); - -test("nn assign wrong password", async ({ page }) => { - test.setTimeout(timeout); - await page.goto("/nn-assign"); - - await expect(page).toHaveTitle(/Assign a Network Number/); - - await page.getByPlaceholder("Install Number").fill("20000"); - await page.getByPlaceholder("Pre-Shared Key").fill("badpassword"); - - await page.getByRole("button", { name: /Submit/i }).click(); - await page.waitForTimeout(clickTimeout); - - await expect(page.locator("[id='alert-network-number']")).toBeHidden(); - await expect(page.locator("[id='assigned-network-number']")).toHaveText(""); -}); - -test("nn assign no password", async ({ page }) => { - test.setTimeout(timeout); - await page.goto("/nn-assign"); - - await expect(page).toHaveTitle(/Assign a Network Number/); - - await page.getByPlaceholder("Install Number").fill("20000"); - - await expect(page.getByRole("button", { name: /Submit/i })).toBeDisabled(); - await page.waitForTimeout(clickTimeout); - - await expect(page.locator("[id='alert-network-number']")).toBeHidden(); - await expect(page.locator("[id='assigned-network-number']")).toHaveText(""); -}); diff --git a/frontend/cluesheet-web/tests/04_join_form_s3_down.spec.ts b/frontend/cluesheet-web/tests/04_join_form_s3_down.spec.ts deleted file mode 100644 index f51d75c..0000000 --- a/frontend/cluesheet-web/tests/04_join_form_s3_down.spec.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { test, expect } from "@playwright/test"; -import { fillOutJoinForm, sampleData, submitFailureExpected } from "./util"; - -const joinFormTimeout = 20000; - -// This test will run in its own github action that does not set up MinIO. -// I tried for WEEKS and I could not find a way to mock a failure between -// Node and MinIO. -test.describe("fail meshdb is hard down and s3 is hard down", () => { - test.skip(process.env.RUN_SKIPPED !== "true"); - - test("fail meshdb is hard down and s3 is hard down", async ({ page }) => { - // Block access to the join form API - await page.route("**/api/v1/join/**", (route) => route.abort()); - - test.setTimeout(joinFormTimeout); - await page.goto("/join"); - - // Is the page title correct? - await expect(page).toHaveTitle(/Join Our Community Network!/); - - // Set up sample data. - await fillOutJoinForm(page, sampleData); - - // Uncomment this if you want to poke around after the join form has been filled out - //await page.pause(); - - await submitFailureExpected(page); - }); -}); diff --git a/frontend/cluesheet-web/tests/05_join_form_i18n.spec.ts b/frontend/cluesheet-web/tests/05_join_form_i18n.spec.ts deleted file mode 100644 index 26c1985..0000000 --- a/frontend/cluesheet-web/tests/05_join_form_i18n.spec.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { JoinFormValues } from "@/components/JoinForm/JoinForm"; -import { test, expect } from "./mock/test"; - -import { - sampleData, - fillOutJoinForm, - submitConfirmationDialogExpected, - expectSuccess, -} from "./util"; - -const joinFormTimeout = 40000; -const unitTestTimeout = 5000; - -test("es confirm street address", async ({ page }) => { - test.setTimeout(joinFormTimeout); - await page.goto("/join"); - - // Is the page title correct? - await expect(page).toHaveTitle(/Join Our Community Network!/); - - await page - .locator("[id='joinform-locale-switcher-select']") - .selectOption("🇪🇸 Español"); - - await expect(page.locator("[id='joinform-title']")).toHaveText( - "Únase NYC Mesh", - ); - - let data: JoinFormValues = Object.assign({}, sampleData); - data.street_address = "197 prospect pl"; - - // Set up sample data. - await fillOutJoinForm(page, data); - - await submitConfirmationDialogExpected(page, 2000); - - // Ensure the dialogue is translated - await expect(page.locator("[id='alert-dialog-description']")).toHaveText( - "Debíamos que reformatear algunos de sus datos. Por favor asegurar que los campos abajos son ascertados.", - ); - - await page.locator("[name='confirm']").click(); - - await expectSuccess(page, unitTestTimeout); -}); diff --git a/frontend/cluesheet-web/tests/lib/data.ts b/frontend/cluesheet-web/tests/lib/data.ts new file mode 100644 index 0000000..5b0c115 --- /dev/null +++ b/frontend/cluesheet-web/tests/lib/data.ts @@ -0,0 +1 @@ +export const mockCluesheetUUID = "3a4d3998-71cb-42ca-942c-202db9e465c4"; diff --git a/frontend/cluesheet-web/tests/mock/handlers.ts b/frontend/cluesheet-web/tests/mock/handlers.ts index 0e09b78..82d30f4 100644 --- a/frontend/cluesheet-web/tests/mock/handlers.ts +++ b/frontend/cluesheet-web/tests/mock/handlers.ts @@ -6,182 +6,22 @@ import { JoinFormValues, } from "@/components/JoinForm/JoinForm"; import { NNAssignRequestValues } from "@/components/NNAssignForm/NNAssignForm"; +import { mockCluesheetUUID } from "../lib/data"; export default [ - http.post("/api/v1/join/", async ({ request }) => { + http.get(`/api/v1/cluesheet/${mockCluesheetUUID}`, async ({ request }) => { console.debug("Hello from mocked join API."); - - const requestJson = await request.json(); - - if (requestJson === undefined || requestJson === null) { - return HttpResponse.json( - { detail: "Mock: Missing request body" }, - { status: 400 }, - ); - } - - // First thing we should do is check captcha if it's enabled - if ( - request.headers.get("X-Recaptcha-V2-Token") === "" && - process.env.RECAPTCHA_V2_KEY - ) { - return HttpResponse.json( - { detail: "Captcha verification failed" }, - { status: 401 }, - ); - } - - const joinRequest: JoinFormValues = requestJson as JoinFormValues; - - const good_response: JoinFormResponse = { - detail: "", - building_id: "a5b316f2-c167-40a1-8f40-dd6b54daf0fe", - member_id: "c9305944-80d8-4b1c-b5b9-3c9c0db0d2a1", - install_id: "d9e13697-8240-4c0c-be86-d9aa67617165", - install_number: 1002, - member_exists: false, - changed_info: {}, - }; - - // Special case to test "trust me bro" - if (joinRequest.street_address === chomSt) { - if (joinRequest.trust_me_bro) { - return HttpResponse.json(good_response, { status: 201 }); - } - - // Else, we're gonna return a 409. - let r = new JoinFormResponse(); - r.detail = "Mock: Please confirm a few details."; - r.changed_info = { street_address: chomStreet }; - - return HttpResponse.json(r, { status: 409 }); - } - - if (!joinRequest.trust_me_bro) { - // Bail on New Jersey - if ( - joinRequest.state === "NJ" || - joinRequest.state === "New Jersey" || - joinRequest.zip_code === "07030" - ) { - let r = new JoinFormResponse(); - r.detail = - "Mock: Non-NYC registrations are not supported at this time."; - - return HttpResponse.json(r, { status: 400 }); - } - - // Return 409 if the street address is improperly formatted - if (joinRequest.street_address === "197 prospect pl") { - let r = new JoinFormResponse(); - r.detail = "Mock: Please confirm a few details."; - r.changed_info = { street_address: "197 Prospect Place" }; - - return HttpResponse.json(r, { status: 409 }); - } - - // Return 409 if the city is improperly formatted - if (joinRequest.city === "brooklyn") { - let r = new JoinFormResponse(); - r.detail = "Mock: Please confirm a few details."; - r.changed_info = { city: "Brooklyn" }; - return HttpResponse.json(r, { status: 409 }); - } - - if (joinRequest.phone_number === "") { - // This is probably the wrong way to do this, but the empty phone number case was - // failing the strict equality check below - - let expectedData: JoinFormValues = Object.assign( - {}, - expectedAPIRequestData, - ); - expectedData.phone_number = ""; - if (isDeepStrictEqual(joinRequest, expectedData)) { - return HttpResponse.json(good_response, { status: 201 }); - } - } - - // If anything else is wrong with the form we got, then bail - if (!isDeepStrictEqual(joinRequest, expectedAPIRequestData)) { - console.error( - "Mock Join API is returning 400. (request is not deeply equal to expectedAPIRequestData)", - ); - console.error("Expected the following:"); - console.error(expectedAPIRequestData); - console.error("Got the follwing:"); - console.error(joinRequest); - - let r = new JoinFormResponse(); - r.detail = "Mock failure. Request does not match expected request."; - return HttpResponse.json(r, { status: 400 }); - } - } - - if (joinRequest.street_address === "333 chom st") { - // wtf we're still here? bail!!! - // TODO: Re-write this mock server because it SUUUUCKS - let r = new JoinFormResponse(); - r.detail = "What the fuck"; - return HttpResponse.json(r, { status: 400 }); - } - // OK we're chilling. Return 200 - return HttpResponse.json(good_response, { status: 201 }); + return HttpResponse.json({ detail: "CSH" }, { status: 201 }); }), http.post("/api/v1/nn-assign/", async ({ request }) => { console.debug("Hello from mocked NN Assign API."); const requestJson = await request.json(); - if (requestJson === undefined || requestJson === null) { - return HttpResponse.json( - { detail: "Mock: Missing request body" }, - { status: 400 }, - ); - } - - const nnAssignRequest: NNAssignRequestValues = - requestJson as NNAssignRequestValues; - - // Firstly, check if we have the right password - if (nnAssignRequest.password != "localdev") { - console.debug("Mock bad password"); - return HttpResponse.json( - { detail: "Mock failure. Authentication Failed." }, - { status: 400 }, - ); - } - - if (nnAssignRequest.install_number == "20000") { - const json = { - detail: "Network Number has been assigned!", - building_id: 69, - install_id: 69, - install_number: 20000, - network_number: 420, - created: true, - }; - - return HttpResponse.json(json, { status: 201 }); - } - - if (nnAssignRequest.install_number == "30000") { - const message = `This Install Number (30000) already has a Network Number (520) associated with it!`; - const json = { - detail: message, - building_id: 79, - install_id: 79, - install_number: 30000, - network_number: 520, - created: false, - }; - return HttpResponse.json(json, { status: 200 }); - } - return HttpResponse.json( - { detail: "Mock failure. Server Error." }, - { status: 500 }, + { detail: "Hello"}, + { status: 200 }, ); }), ]; diff --git a/frontend/cluesheet-web/tests/mock_meshdb_500/handlers.ts b/frontend/cluesheet-web/tests/mock_meshdb_500/handlers.ts deleted file mode 100644 index 8a8e55e..0000000 --- a/frontend/cluesheet-web/tests/mock_meshdb_500/handlers.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { http, HttpResponse } from "msw"; -import { JoinFormResponse } from "@/components/JoinForm/JoinForm"; - -export default [ - http.post("/api/v1/join/", async ({ request }) => { - console.debug("Hello from mocked join API. I'm gonna return a 500 now :)"); - - let r = new JoinFormResponse(); - r.detail = "Mock: A server error has occurred."; - - return HttpResponse.json(r, { status: 500 }); - }), - http.post("/api/v1/nn-assign/", async ({ request }) => { - console.debug("Hello from mocked NN Assign API."); - - return HttpResponse.json( - { detail: "Mock: A server error has occurred." }, - { status: 500 }, - ); - }), -]; diff --git a/frontend/cluesheet-web/tests/mock_meshdb_500/test.ts b/frontend/cluesheet-web/tests/mock_meshdb_500/test.ts deleted file mode 100644 index f7c7d20..0000000 --- a/frontend/cluesheet-web/tests/mock_meshdb_500/test.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { test as base, expect } from "@playwright/test"; -import { http } from "msw"; -import type { MockServiceWorker } from "playwright-msw"; -import { createWorkerFixture } from "playwright-msw"; - -import handlers from "./handlers"; - -const test = base.extend<{ - worker: MockServiceWorker; - http: typeof http; -}>({ - worker: createWorkerFixture(handlers), - http, -}); - -export { expect, test }; diff --git a/frontend/cluesheet-web/tests/util.ts b/frontend/cluesheet-web/tests/util.ts deleted file mode 100644 index 01c0d72..0000000 --- a/frontend/cluesheet-web/tests/util.ts +++ /dev/null @@ -1,185 +0,0 @@ -import { getJoinRecordFromS3 } from "@/lib/join_record"; -import { JoinRecord } from "@/lib/types"; -import { JoinFormValues } from "@/components/JoinForm/JoinForm"; -import { expect, Page } from "@playwright/test"; - -export const chomSt = "333 chom st"; -export const chomStreet = "333 Chom Street"; - -export const sampleData: JoinFormValues = { - first_name: "Jon", - last_name: "Smith", - email_address: "js@gmail.com", - phone_number: "585-475-2411", - street_address: "197 Prospect Place", - apartment: "1", - city: "Brooklyn", - state: "NY", - zip_code: "11238", - roof_access: true, - referral: "Mock Sample Data", - ncl: true, - trust_me_bro: false, -}; - -export const sampleJoinRecord: JoinRecord = { - first_name: "Jon", - last_name: "Smith", - email_address: "js@gmail.com", - phone_number: "+1 585-475-2411", - street_address: "197 Prospect Place", - apartment: "1", - city: "Brooklyn", - state: "NY", - zip_code: "11238", - roof_access: true, - referral: "Mock Sample Data", - ncl: true, - trust_me_bro: false, - version: 3, - uuid: "1a55b949-0490-4b78-a2e8-10aea41d6f1d", - submission_time: "2024-11-01T08:24:24", - code: 201, - replayed: 0, - install_number: 1002, -}; - -export const expectedTrustMeBroValues: JoinFormValues = { - first_name: "Jon", - last_name: "Smith", - email_address: "js@gmail.com", - phone_number: "585-475-2411", - street_address: "197 prospect pl", - apartment: "1", - city: "brooklyn", - state: "NY", - zip_code: "11238", - roof_access: true, - referral: "Mock Sample Data", - ncl: true, - trust_me_bro: false, -}; - -export const sampleNJData: JoinFormValues = { - first_name: "Jon", - last_name: "Smith", - email_address: "js@gmail.com", - phone_number: "585-475-2411", - street_address: "711 Hudson Street", - city: "Hoboken", - state: "NJ", - zip_code: "07030", - apartment: "1", - roof_access: true, - referral: "Mock Sample Data", - ncl: true, - trust_me_bro: false, -}; - -let expectedAPIRequestDataMut: JoinFormValues = sampleData; -expectedAPIRequestDataMut.phone_number = "+1 585-475-2411"; - -export const expectedAPIRequestData = expectedAPIRequestDataMut; - -export async function fillOutJoinForm(page: Page, sampleData: JoinFormValues) { - // Set up some sample data - - // Personal info - await page.locator("[name='first_name']").fill(sampleData.first_name); - await page.locator("[name='last_name']").fill(sampleData.last_name); - await page.locator("[name='email_address']").fill(sampleData.email_address); - await page.locator("[name='phone_number']").fill(sampleData.phone_number); - - // Address Info - await page.locator("[name='street_address']").fill(sampleData.street_address); - await page.locator("[name='apartment']").fill(sampleData.apartment); - await page.locator("[name='city']").fill(sampleData.city); - await page.locator("[name='state']").fill(sampleData.state); - await page.locator("[name='zip_code']").fill(sampleData.zip_code.toString()); - - // How did you hear about us? - await page.locator("[name='referral']").fill(sampleData.referral); - - // Agree to the NCL - if (sampleData.ncl) { - await page.locator("[name='ncl']").check(); - } - - // Roof Access - if (sampleData.roof_access) { - await page.locator("[name='roof_access']").check(); - } -} - -export async function submitFailureExpected(page: Page) { - await page.waitForTimeout(1000); - // Submit the join form - await page.locator("[name='submit_join_form']").click(); - - await page.waitForTimeout(1000); - - // The submission should've been stopped, and the member should be able to try again. - await expect(page.locator("[name='submit_join_form']")).toHaveText("Submit"); -} - -export async function submitAndCheckToast(page: Page, toastMessage: string) { - await page.waitForTimeout(1000); - // Submit the join form - await page.locator("[name='submit_join_form']").click(); - - await page.waitForTimeout(1000); - - // The submission should've been stopped, and the member should be able to try again. - await expect(page.locator("[name='submit_join_form']")).toHaveText("Submit"); - await expect(page.getByTestId("toasty")).toContainText(toastMessage); -} - -export async function submitSuccessExpected( - page: Page, - timeout: number = 10000, -) { - await page.waitForTimeout(1000); - // Listen for all console logs - page.on("console", (msg) => console.log(msg.text())); - - // Submit the join form - await page.locator("[name='submit_join_form']").click(); - - await expectSuccess(page, timeout); -} - -export async function expectSuccess(page: Page, timeout: number = 1000) { - await page.waitForTimeout(timeout); - await expect(page.locator("[name='submit_join_form']")).toBeHidden(); - await expect(page.locator("[id='alert-thank-you']")).toBeVisible(); -} - -export async function submitConfirmationDialogExpected( - page: Page, - timeout: number = 10000, -) { - // Listen for all console logs - page.on("console", (msg) => console.log(msg.text())); - - // Submit the join form - await page.locator("[name='submit_join_form']").click(); - - await page.waitForTimeout(timeout); - await expect(page.locator("[id='alert-dialog-title']")).toBeVisible(); -} - -export async function findJoinRecord(page: Page): Promise { - const joinRecordKey = await page.getAttribute( - '[data-testid="test-join-record-key"]', - "data-state", - ); - - if (joinRecordKey === null) { - throw new Error("Got null join record"); - } - - // This is wacky because playwright has to run this and therefore - // needs our dotenv. - const joinRecord: JoinRecord = await getJoinRecordFromS3(joinRecordKey); - return joinRecord; -} From 8f4d0bdae4335e664df6b3774e476999cd9c1896 Mon Sep 17 00:00:00 2001 From: Will Nilges Date: Sun, 4 May 2025 16:55:45 -0400 Subject: [PATCH 06/12] change a million things --- .../.github/workflows/playwright.yml | 36 ++++++------- frontend/cluesheet-web/package-lock.json | 24 +++++++++ frontend/cluesheet-web/package.json | 1 + frontend/cluesheet-web/playwright.config.ts | 20 +++---- .../cluesheet/[cluesheet_id]/page.module.scss | 13 +++++ .../src/app/cluesheet/[cluesheet_id]/page.tsx | 53 +++++++++++++++++-- .../src/components/viewer/ClueGroup.tsx | 0 .../src/components/viewer/ClueList.tsx | 3 -- .../src/components/viewer/LineItem.tsx | 0 .../components/viewer/cluegroup/ClueGroup.tsx | 30 +++++++++++ .../viewer/cluegroup/page.module.scss | 23 ++++++++ .../viewer/counter/PointCounter.tsx | 24 +++++++++ .../viewer/counter/page.module.scss | 16 ++++++ .../cluesheet-web/src/lib/Configuration.ts | 5 +- frontend/cluesheet-web/src/lib/endpoint.ts | 4 +- frontend/cluesheet-web/src/lib/types.ts | 8 +++ frontend/cluesheet-web/tests/mock/handlers.ts | 9 +--- 17 files changed, 224 insertions(+), 45 deletions(-) create mode 100644 frontend/cluesheet-web/src/app/cluesheet/[cluesheet_id]/page.module.scss delete mode 100644 frontend/cluesheet-web/src/components/viewer/ClueGroup.tsx delete mode 100644 frontend/cluesheet-web/src/components/viewer/ClueList.tsx delete mode 100644 frontend/cluesheet-web/src/components/viewer/LineItem.tsx create mode 100644 frontend/cluesheet-web/src/components/viewer/cluegroup/ClueGroup.tsx create mode 100644 frontend/cluesheet-web/src/components/viewer/cluegroup/page.module.scss create mode 100644 frontend/cluesheet-web/src/components/viewer/counter/PointCounter.tsx create mode 100644 frontend/cluesheet-web/src/components/viewer/counter/page.module.scss create mode 100644 frontend/cluesheet-web/src/lib/types.ts diff --git a/frontend/cluesheet-web/.github/workflows/playwright.yml b/frontend/cluesheet-web/.github/workflows/playwright.yml index 3eb1314..2812391 100644 --- a/frontend/cluesheet-web/.github/workflows/playwright.yml +++ b/frontend/cluesheet-web/.github/workflows/playwright.yml @@ -1,27 +1,27 @@ name: Playwright Tests on: push: - branches: [ main, master ] + branches: [main, master] pull_request: - branches: [ main, master ] + 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 ci - - name: Install Playwright Browsers - run: npx playwright install --with-deps - - name: Run Playwright tests - run: npx playwright test - - uses: actions/upload-artifact@v4 - if: ${{ !cancelled() }} - with: - name: playwright-report - path: playwright-report/ - retention-days: 30 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: lts/* + - name: Install dependencies + run: npm ci + - name: Install Playwright Browsers + run: npx playwright install --with-deps + - name: Run Playwright tests + run: npx playwright test + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/frontend/cluesheet-web/package-lock.json b/frontend/cluesheet-web/package-lock.json index 42efdfe..7894b00 100644 --- a/frontend/cluesheet-web/package-lock.json +++ b/frontend/cluesheet-web/package-lock.json @@ -16,6 +16,7 @@ "powerhooks": "^2.0.1", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-flip-numbers": "^3.0.9", "react-router-dom": "^7.5.3", "reactstrap": "^9.2.3" }, @@ -2073,6 +2074,20 @@ "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==", "license": "MIT" }, + "node_modules/react-flip-numbers": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/react-flip-numbers/-/react-flip-numbers-3.0.9.tgz", + "integrity": "sha512-ZFpFIz5wxT00rPuMfmCfPOsnC09u9oxYV3zPlfG1X79N58a5bwFvZcfl6BIosowa2tI9t9KyJIYcJICFyr5Y5g==", + "license": "Mit", + "dependencies": { + "react-simple-animate": "^3.0.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19", + "react-dom": "^16.8.0 || ^17 || ^18 || ^19", + "react-simple-animate": "^3.0.1" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -2118,6 +2133,15 @@ "react-dom": ">=18" } }, + "node_modules/react-simple-animate": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/react-simple-animate/-/react-simple-animate-3.5.3.tgz", + "integrity": "sha512-Ob+SmB5J1tXDEZyOe2Hf950K4M8VaWBBmQ3cS2BUnTORqHjhK0iKG8fB+bo47ZL15t8d3g/Y0roiqH05UBjG7A==", + "license": "MIT", + "peerDependencies": { + "react-dom": "^16.8.0 || ^17 || ^18 || ^19" + } + }, "node_modules/react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", diff --git a/frontend/cluesheet-web/package.json b/frontend/cluesheet-web/package.json index b78f5c9..889a595 100644 --- a/frontend/cluesheet-web/package.json +++ b/frontend/cluesheet-web/package.json @@ -17,6 +17,7 @@ "powerhooks": "^2.0.1", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-flip-numbers": "^3.0.9", "react-router-dom": "^7.5.3", "reactstrap": "^9.2.3" }, diff --git a/frontend/cluesheet-web/playwright.config.ts b/frontend/cluesheet-web/playwright.config.ts index a05d8b5..1615efb 100644 --- a/frontend/cluesheet-web/playwright.config.ts +++ b/frontend/cluesheet-web/playwright.config.ts @@ -1,4 +1,4 @@ -import { defineConfig, devices } from '@playwright/test'; +import { defineConfig, devices } from "@playwright/test"; /** * Read environment variables from file. @@ -12,7 +12,7 @@ import { defineConfig, devices } from '@playwright/test'; * See https://playwright.dev/docs/test-configuration. */ export default defineConfig({ - testDir: './tests', + 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. */ @@ -22,31 +22,31 @@ export default defineConfig({ /* 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', + 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://127.0.0.1:3000', /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ - trace: 'on-first-retry', + trace: "on-first-retry", }, /* Configure projects for major browsers */ projects: [ { - name: 'chromium', - use: { ...devices['Desktop Chrome'] }, + name: "chromium", + use: { ...devices["Desktop Chrome"] }, }, { - name: 'firefox', - use: { ...devices['Desktop Firefox'] }, + name: "firefox", + use: { ...devices["Desktop Firefox"] }, }, { - name: 'webkit', - use: { ...devices['Desktop Safari'] }, + name: "webkit", + use: { ...devices["Desktop Safari"] }, }, /* Test against mobile viewports. */ diff --git a/frontend/cluesheet-web/src/app/cluesheet/[cluesheet_id]/page.module.scss b/frontend/cluesheet-web/src/app/cluesheet/[cluesheet_id]/page.module.scss new file mode 100644 index 0000000..246c9ee --- /dev/null +++ b/frontend/cluesheet-web/src/app/cluesheet/[cluesheet_id]/page.module.scss @@ -0,0 +1,13 @@ +.header { + display: flex; + justify-content: space-between; + padding: 2em; +} + +.clueList { + display: flex; + flex: 1; + flex-grow: 1; + + border-radius: 20px; +} diff --git a/frontend/cluesheet-web/src/app/cluesheet/[cluesheet_id]/page.tsx b/frontend/cluesheet-web/src/app/cluesheet/[cluesheet_id]/page.tsx index 767c6b6..bb36053 100644 --- a/frontend/cluesheet-web/src/app/cluesheet/[cluesheet_id]/page.tsx +++ b/frontend/cluesheet-web/src/app/cluesheet/[cluesheet_id]/page.tsx @@ -1,4 +1,10 @@ import { cluesheetBackendEndpoint } from "@/lib/endpoint"; +import styles from "./page.module.scss"; +import { Container } from "reactstrap"; +import FlipNumbers from "react-flip-numbers"; +import PointCounter from "@/components/viewer/counter/PointCounter"; +import { Clue } from "@/lib/types"; +import ClueGroup from "@/components/viewer/cluegroup/ClueGroup"; export const metadata = { title: "View cluesheet", @@ -10,15 +16,54 @@ export default async function CluesheetViewer({ }: { params: Promise<{ cluesheet_id: string }>; }) { - const { cluesheet_id } = await params; + //const { cluesheet_id } = await params; + //const cluesheet = await fetch(`${cluesheetBackendEndpoint}/${cluesheet_id}`); + //console.log(`Got cluesheet object: ${await cluesheet.json()}`); - const cluesheet = await fetch(`${cluesheetBackendEndpoint}/${cluesheet_id}`); + const cluesheet = { + title: "Chom skz", + user_points: 69420, + user_additional_score: ["-Your Bones", "+Adam Neulight's Car"], + /*point_unit: "Kubernetes Clusters",*/ + clues: [ + { + id: "my-sick-and-poggers-uuid", + limit: 1, + completions: 1, + description: "Eat a whole can of beans", + points: "+5", + children: [ + { + id: "my-sick-and-poggers-uuid-2", + limit: 1, + completions: 1, + description: "With a fork", + points: "+1", + children: [], + }, + ], + }, + ], + }; - console.log(`Got cluesheet object: ${await cluesheet.json()}`); - return ( <>
+ +
+

Cluesheet Name

+
+ +

Points

+
+
+
+ {cluesheet.clues.length > 0 && + cluesheet.clues.map((clue: Clue, index) => ( + + ))} +
+
); diff --git a/frontend/cluesheet-web/src/components/viewer/ClueGroup.tsx b/frontend/cluesheet-web/src/components/viewer/ClueGroup.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/frontend/cluesheet-web/src/components/viewer/ClueList.tsx b/frontend/cluesheet-web/src/components/viewer/ClueList.tsx deleted file mode 100644 index 5f5be16..0000000 --- a/frontend/cluesheet-web/src/components/viewer/ClueList.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function ClueList() { - -} diff --git a/frontend/cluesheet-web/src/components/viewer/LineItem.tsx b/frontend/cluesheet-web/src/components/viewer/LineItem.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/frontend/cluesheet-web/src/components/viewer/cluegroup/ClueGroup.tsx b/frontend/cluesheet-web/src/components/viewer/cluegroup/ClueGroup.tsx new file mode 100644 index 0000000..f77aaac --- /dev/null +++ b/frontend/cluesheet-web/src/components/viewer/cluegroup/ClueGroup.tsx @@ -0,0 +1,30 @@ +"use client" +import { Clue } from "@/lib/types"; +import styles from "./page.module.scss"; + +interface ClueGroupProps { + clue: Clue; +} + +export default function ClueGroup({ clue }: ClueGroupProps) { + function handleClueChecked() { + console.log("Checked!"); + } + + return ( + <> +
+
+ 0 ? true : false}/> +

{clue.points}: {clue.description}

+
+
+ {clue.children.length > 0 && + clue.children.map((clue: Clue, index) => ( + + ))} +
+
+ + ); +} diff --git a/frontend/cluesheet-web/src/components/viewer/cluegroup/page.module.scss b/frontend/cluesheet-web/src/components/viewer/cluegroup/page.module.scss new file mode 100644 index 0000000..97b840d --- /dev/null +++ b/frontend/cluesheet-web/src/components/viewer/cluegroup/page.module.scss @@ -0,0 +1,23 @@ +.clueBody { + display: flex; + flex-direction: column; + flex: 1; + margin: 10px; + + border: 1px solid #ccc; /* Optional border */ + padding: 20px; /* Optional padding */ + border-radius: 8px; /* Optional rounded corners */ +} + +.lineItem { + display: flex; + flex-direction: row; +} + +.lineItem input[type="checkbox"] { + margin: 10px; +} + +.children { + margin-left: 50px; +} diff --git a/frontend/cluesheet-web/src/components/viewer/counter/PointCounter.tsx b/frontend/cluesheet-web/src/components/viewer/counter/PointCounter.tsx new file mode 100644 index 0000000..33e2740 --- /dev/null +++ b/frontend/cluesheet-web/src/components/viewer/counter/PointCounter.tsx @@ -0,0 +1,24 @@ +"use client"; +import FlipNumbers from "react-flip-numbers"; +import styles from "./page.module.scss"; + +interface PointCounterProps { + points: number; +} + +export default function PointCounter({ points }: PointCounterProps) { + return ( +
+ +
+ ); +}; diff --git a/frontend/cluesheet-web/src/components/viewer/counter/page.module.scss b/frontend/cluesheet-web/src/components/viewer/counter/page.module.scss new file mode 100644 index 0000000..3d4116a --- /dev/null +++ b/frontend/cluesheet-web/src/components/viewer/counter/page.module.scss @@ -0,0 +1,16 @@ +@import url("https://fonts.googleapis.com/css2?family=Courier+Prime:ital,wght@0,400;0,700;1,400;1,700&display=swap"); +.flipNumbers { + background-color: black; + border-width: 1px; + border-style: solid; + border-radius: 5px; +} + +.number { + background-color: white; + margin: 1px; + border-radius: 5px; + font-family: "Courier Prime", monospace; + font-weight: 400; + font-style: normal; +} diff --git a/frontend/cluesheet-web/src/lib/Configuration.ts b/frontend/cluesheet-web/src/lib/Configuration.ts index b2c5f12..8944939 100644 --- a/frontend/cluesheet-web/src/lib/Configuration.ts +++ b/frontend/cluesheet-web/src/lib/Configuration.ts @@ -1,5 +1,7 @@ import { OidcConfiguration } from "@axa-fr/react-oidc"; +// FIXME: React is bitching about this +/* const oidcConfig: OidcConfiguration = { client_id: process.env.NEXT_PUBLIC_SSO_CLIENT_ID ?? "test", redirect_uri: `${window.location.protocol}//${window.location.hostname}${ @@ -15,10 +17,11 @@ const oidcConfig: OidcConfiguration = { service_worker_only: false, refresh_time_before_tokens_expiration_in_second: 10, }; +*/ export const apiPrefix = process.env.NEXT_PUBLIC_API_PREFIX; export const SSOEnabled = false; // (import.meta.env.VITE_SSO_ENABLED ?? "true") === "true"; -export default oidcConfig; +//export default oidcConfig; diff --git a/frontend/cluesheet-web/src/lib/endpoint.ts b/frontend/cluesheet-web/src/lib/endpoint.ts index 00d6104..74633e3 100644 --- a/frontend/cluesheet-web/src/lib/endpoint.ts +++ b/frontend/cluesheet-web/src/lib/endpoint.ts @@ -1,2 +1,2 @@ -export const cluesheetBackendEndpoint = process.env.NEXT_PUBLIC_CLUESHEET_BACKEND_ENDPOINT ?? ""; - +export const cluesheetBackendEndpoint = + process.env.NEXT_PUBLIC_CLUESHEET_BACKEND_ENDPOINT ?? ""; diff --git a/frontend/cluesheet-web/src/lib/types.ts b/frontend/cluesheet-web/src/lib/types.ts new file mode 100644 index 0000000..39756f2 --- /dev/null +++ b/frontend/cluesheet-web/src/lib/types.ts @@ -0,0 +1,8 @@ +export type Clue = { + id: string; + limit: number; + completions: number; + description: string; + points: string; + children: Array; +}; diff --git a/frontend/cluesheet-web/tests/mock/handlers.ts b/frontend/cluesheet-web/tests/mock/handlers.ts index 82d30f4..4e36100 100644 --- a/frontend/cluesheet-web/tests/mock/handlers.ts +++ b/frontend/cluesheet-web/tests/mock/handlers.ts @@ -1,11 +1,4 @@ import { http, HttpResponse } from "msw"; -import { chomSt, chomStreet, expectedAPIRequestData } from "../util"; -import { isDeepStrictEqual } from "util"; -import { - JoinFormResponse, - JoinFormValues, -} from "@/components/JoinForm/JoinForm"; -import { NNAssignRequestValues } from "@/components/NNAssignForm/NNAssignForm"; import { mockCluesheetUUID } from "../lib/data"; export default [ @@ -14,6 +7,7 @@ export default [ // OK we're chilling. Return 200 return HttpResponse.json({ detail: "CSH" }, { status: 201 }); }), + /* http.post("/api/v1/nn-assign/", async ({ request }) => { console.debug("Hello from mocked NN Assign API."); @@ -24,4 +18,5 @@ export default [ { status: 200 }, ); }), + */ ]; From f3b6d9ffef611d6faf605650991445ab0041a85d Mon Sep 17 00:00:00 2001 From: Will Nilges Date: Sun, 4 May 2025 17:34:04 -0400 Subject: [PATCH 07/12] checkpoint --- .../cluesheet/[cluesheet_id]/page.module.scss | 1 + .../src/app/cluesheet/[cluesheet_id]/page.tsx | 51 ++++++++++++++++--- .../components/viewer/cluegroup/ClueGroup.tsx | 24 ++++----- .../viewer/cluegroup/page.module.scss | 13 +---- .../viewer/counter/PointCounter.tsx | 2 +- .../components/viewer/lineitem/LineItem.tsx | 34 +++++++++++++ .../viewer/lineitem/page.module.scss | 39 ++++++++++++++ 7 files changed, 129 insertions(+), 35 deletions(-) create mode 100644 frontend/cluesheet-web/src/components/viewer/lineitem/LineItem.tsx create mode 100644 frontend/cluesheet-web/src/components/viewer/lineitem/page.module.scss diff --git a/frontend/cluesheet-web/src/app/cluesheet/[cluesheet_id]/page.module.scss b/frontend/cluesheet-web/src/app/cluesheet/[cluesheet_id]/page.module.scss index 246c9ee..7590007 100644 --- a/frontend/cluesheet-web/src/app/cluesheet/[cluesheet_id]/page.module.scss +++ b/frontend/cluesheet-web/src/app/cluesheet/[cluesheet_id]/page.module.scss @@ -6,6 +6,7 @@ .clueList { display: flex; + flex-direction: column; flex: 1; flex-grow: 1; diff --git a/frontend/cluesheet-web/src/app/cluesheet/[cluesheet_id]/page.tsx b/frontend/cluesheet-web/src/app/cluesheet/[cluesheet_id]/page.tsx index bb36053..7725163 100644 --- a/frontend/cluesheet-web/src/app/cluesheet/[cluesheet_id]/page.tsx +++ b/frontend/cluesheet-web/src/app/cluesheet/[cluesheet_id]/page.tsx @@ -17,11 +17,12 @@ export default async function CluesheetViewer({ params: Promise<{ cluesheet_id: string }>; }) { //const { cluesheet_id } = await params; - //const cluesheet = await fetch(`${cluesheetBackendEndpoint}/${cluesheet_id}`); + //const cluesheet = await fetch(`${cluesheetBackendEndpoint}/api/v1/cluesheet/${cluesheet_id}`); //console.log(`Got cluesheet object: ${await cluesheet.json()}`); const cluesheet = { - title: "Chom skz", + id: "my-epic-cluesheet-uuid", + title: "Opcommathon 2069 Cluesheet '>w<'", user_points: 69420, user_additional_score: ["-Your Bones", "+Adam Neulight's Car"], /*point_unit: "Kubernetes Clusters",*/ @@ -36,13 +37,47 @@ export default async function CluesheetViewer({ { id: "my-sick-and-poggers-uuid-2", limit: 1, - completions: 1, - description: "With a fork", + completions: 0, + description: "No utensils", points: "+1", - children: [], + children: [ + { + id: "my-sick-and-poggers-uuid-3", + limit: 1, + completions: 0, + description: "With a straw", + points: "+1", + children: [ + { + id: "my-sick-and-poggers-uuid-4", + limit: 1, + completions: 0, + description: "Is a straw a utensil?", + points: "+1", + children: [], + }, + ], + }, + ], }, + { + id: "my-sick-and-poggers-uuid-5", + limit: 1, + completions: 0, + description: "On a bike", + points: "+1", + children: [], + } ], }, + { + id: "my-sick-and-poggers-uuid-6", + limit: 0, + completions: 3, + description: "Fix something broken (stacks)", + points: "+10", + children: [], + }, ], }; @@ -51,16 +86,16 @@ export default async function CluesheetViewer({
-

Cluesheet Name

+

{cluesheet.title}

- +

Points

{cluesheet.clues.length > 0 && cluesheet.clues.map((clue: Clue, index) => ( - + ))}
diff --git a/frontend/cluesheet-web/src/components/viewer/cluegroup/ClueGroup.tsx b/frontend/cluesheet-web/src/components/viewer/cluegroup/ClueGroup.tsx index f77aaac..c49cdd8 100644 --- a/frontend/cluesheet-web/src/components/viewer/cluegroup/ClueGroup.tsx +++ b/frontend/cluesheet-web/src/components/viewer/cluegroup/ClueGroup.tsx @@ -1,30 +1,24 @@ -"use client" +"use client"; import { Clue } from "@/lib/types"; import styles from "./page.module.scss"; +import LineItem from "../lineitem/LineItem"; interface ClueGroupProps { clue: Clue; } export default function ClueGroup({ clue }: ClueGroupProps) { - function handleClueChecked() { - console.log("Checked!"); - } - return ( <>
-
- 0 ? true : false}/> -

{clue.points}: {clue.description}

-
+
- {clue.children.length > 0 && - clue.children.map((clue: Clue, index) => ( - - ))} -
-
+ {clue.children.length > 0 && + clue.children.map((clue: Clue, index) => ( + + ))} + + ); } diff --git a/frontend/cluesheet-web/src/components/viewer/cluegroup/page.module.scss b/frontend/cluesheet-web/src/components/viewer/cluegroup/page.module.scss index 97b840d..f611f1e 100644 --- a/frontend/cluesheet-web/src/components/viewer/cluegroup/page.module.scss +++ b/frontend/cluesheet-web/src/components/viewer/cluegroup/page.module.scss @@ -5,19 +5,10 @@ margin: 10px; border: 1px solid #ccc; /* Optional border */ - padding: 20px; /* Optional padding */ + padding: 10px; /* Optional padding */ border-radius: 8px; /* Optional rounded corners */ } -.lineItem { - display: flex; - flex-direction: row; -} - -.lineItem input[type="checkbox"] { - margin: 10px; -} - .children { - margin-left: 50px; + margin-left: 20px; } diff --git a/frontend/cluesheet-web/src/components/viewer/counter/PointCounter.tsx b/frontend/cluesheet-web/src/components/viewer/counter/PointCounter.tsx index 33e2740..364a66d 100644 --- a/frontend/cluesheet-web/src/components/viewer/counter/PointCounter.tsx +++ b/frontend/cluesheet-web/src/components/viewer/counter/PointCounter.tsx @@ -21,4 +21,4 @@ export default function PointCounter({ points }: PointCounterProps) { /> ); -}; +} diff --git a/frontend/cluesheet-web/src/components/viewer/lineitem/LineItem.tsx b/frontend/cluesheet-web/src/components/viewer/lineitem/LineItem.tsx new file mode 100644 index 0000000..26a87e1 --- /dev/null +++ b/frontend/cluesheet-web/src/components/viewer/lineitem/LineItem.tsx @@ -0,0 +1,34 @@ +import { Clue } from "@/lib/types"; + +import styles from "./page.module.scss"; +import { useState } from "react"; +interface LineItemProps { + clue: Clue; +} + +export default function LineItem({ clue }: LineItemProps) { + // Initialize state to track if the checkbox is checked + const [isChecked, setIsChecked] = useState(Number(clue.completions) > 0); + + // Handle checkbox change + const handleClueChecked = () => { + setIsChecked(prevChecked => !prevChecked); + // You can also perform any additional logic here, like updating the clue state + }; + + return ( + <> +
+ +
+ {clue.points}: {clue.description} +
+
+ + ); +} diff --git a/frontend/cluesheet-web/src/components/viewer/lineitem/page.module.scss b/frontend/cluesheet-web/src/components/viewer/lineitem/page.module.scss new file mode 100644 index 0000000..f1030f9 --- /dev/null +++ b/frontend/cluesheet-web/src/components/viewer/lineitem/page.module.scss @@ -0,0 +1,39 @@ +.lineItem { + display: flex; + flex-direction: row; +} + +/* Hide the default checkbox */ +.customCheckboxInput { + display: none; +} + +/* Create a custom checkbox */ +.customCheckboxInput { + width: 24px; /* Size of the checkbox */ + height: 24px; /* Size of the checkbox */ + border: 2px solid #ccc; /* Border color */ + display: inline-block; + position: relative; + cursor: pointer; +} + + +/* Add a checkmark */ +.customCheckboxInput::after { + content: ''; + position: absolute; + left: 6px; /* Position of the checkmark */ + top: 2px; /* Position of the checkmark */ + width: 6px; /* Width of the checkmark */ + height: 12px; /* Height of the checkmark */ + border: solid white; /* Checkmark color */ + border-width: 0 2px 2px 0; /* Checkmark shape */ + transform: rotate(45deg); + opacity: 0; /* Initially hidden */ +} + +/* Show the checkmark when checked */ +.customCheckboxInput:checked + .customCheckbox::after { + opacity: 1; /* Show checkmark */ +} From 24ec49549a12c9b7daa47b8a19f39ac9960223b1 Mon Sep 17 00:00:00 2001 From: Will Nilges Date: Sun, 4 May 2025 18:48:00 -0400 Subject: [PATCH 08/12] checkpoint --- .../src/app/cluesheet/[cluesheet_id]/page.tsx | 21 ++--- .../user/[user_id]/page.module.scss | 14 ++++ .../[cluesheet_id]/user/[user_id]/page.tsx | 45 +++++++++++ .../components/viewer/cluegroup/ClueGroup.tsx | 4 +- .../components/viewer/lineitem/LineItem.tsx | 2 +- .../viewer/lineitem/page.module.scss | 38 +++++----- frontend/cluesheet-web/src/lib/types.ts | 10 ++- frontend/cluesheet-web/tests/lib/data.ts | 76 +++++++++++++++++++ 8 files changed, 171 insertions(+), 39 deletions(-) create mode 100644 frontend/cluesheet-web/src/app/cluesheet/[cluesheet_id]/user/[user_id]/page.module.scss create mode 100644 frontend/cluesheet-web/src/app/cluesheet/[cluesheet_id]/user/[user_id]/page.tsx diff --git a/frontend/cluesheet-web/src/app/cluesheet/[cluesheet_id]/page.tsx b/frontend/cluesheet-web/src/app/cluesheet/[cluesheet_id]/page.tsx index 7725163..45f4d38 100644 --- a/frontend/cluesheet-web/src/app/cluesheet/[cluesheet_id]/page.tsx +++ b/frontend/cluesheet-web/src/app/cluesheet/[cluesheet_id]/page.tsx @@ -23,35 +23,28 @@ export default async function CluesheetViewer({ const cluesheet = { id: "my-epic-cluesheet-uuid", title: "Opcommathon 2069 Cluesheet '>w<'", - user_points: 69420, - user_additional_score: ["-Your Bones", "+Adam Neulight's Car"], - /*point_unit: "Kubernetes Clusters",*/ clues: [ { id: "my-sick-and-poggers-uuid", limit: 1, - completions: 1, description: "Eat a whole can of beans", points: "+5", children: [ { id: "my-sick-and-poggers-uuid-2", limit: 1, - completions: 0, description: "No utensils", points: "+1", children: [ { id: "my-sick-and-poggers-uuid-3", limit: 1, - completions: 0, description: "With a straw", points: "+1", children: [ { id: "my-sick-and-poggers-uuid-4", limit: 1, - completions: 0, description: "Is a straw a utensil?", points: "+1", children: [], @@ -63,20 +56,18 @@ export default async function CluesheetViewer({ { id: "my-sick-and-poggers-uuid-5", limit: 1, - completions: 0, description: "On a bike", points: "+1", children: [], - } + }, ], }, { - id: "my-sick-and-poggers-uuid-6", - limit: 0, - completions: 3, - description: "Fix something broken (stacks)", - points: "+10", - children: [], + id: "my-sick-and-poggers-uuid-6", + limit: 0, + description: "Fix something broken (stacks)", + points: "+10", + children: [], }, ], }; diff --git a/frontend/cluesheet-web/src/app/cluesheet/[cluesheet_id]/user/[user_id]/page.module.scss b/frontend/cluesheet-web/src/app/cluesheet/[cluesheet_id]/user/[user_id]/page.module.scss new file mode 100644 index 0000000..7590007 --- /dev/null +++ b/frontend/cluesheet-web/src/app/cluesheet/[cluesheet_id]/user/[user_id]/page.module.scss @@ -0,0 +1,14 @@ +.header { + display: flex; + justify-content: space-between; + padding: 2em; +} + +.clueList { + display: flex; + flex-direction: column; + flex: 1; + flex-grow: 1; + + border-radius: 20px; +} diff --git a/frontend/cluesheet-web/src/app/cluesheet/[cluesheet_id]/user/[user_id]/page.tsx b/frontend/cluesheet-web/src/app/cluesheet/[cluesheet_id]/user/[user_id]/page.tsx new file mode 100644 index 0000000..515040f --- /dev/null +++ b/frontend/cluesheet-web/src/app/cluesheet/[cluesheet_id]/user/[user_id]/page.tsx @@ -0,0 +1,45 @@ +import { cluesheetBackendEndpoint } from "@/lib/endpoint"; +import styles from "./page.module.scss"; +import { Container } from "reactstrap"; +import FlipNumbers from "react-flip-numbers"; +import PointCounter from "@/components/viewer/counter/PointCounter"; +import { Clue } from "@/lib/types"; +import ClueGroup from "@/components/viewer/cluegroup/ClueGroup"; +import { mockCluesheet } from "../../../../../../tests/lib/data"; + +export const metadata = { + title: "View cluesheet", + description: "View an mf cluesheet kerchoo", +}; + +export default async function CluesheetEditor({ + params, +}: { + params: Promise<{ cluesheet_id: string; keycloak_uid: string }>; +}) { + //const { cluesheet_id } = await params; + //const cluesheet = await fetch(`${cluesheetBackendEndpoint}/api/v1/cluesheet/${cluesheet_id}`); + //console.log(`Got cluesheet object: ${await cluesheet.json()}`); + + return ( + <> +
+ +
+

{mockCluesheet.title}

+
+ +

Points

+
+
+
+ {mockCluesheet.clues.length > 0 && + mockCluesheet.clues.map((clue: Clue) => ( + + ))} +
+
+
+ + ); +} diff --git a/frontend/cluesheet-web/src/components/viewer/cluegroup/ClueGroup.tsx b/frontend/cluesheet-web/src/components/viewer/cluegroup/ClueGroup.tsx index c49cdd8..ae5edb5 100644 --- a/frontend/cluesheet-web/src/components/viewer/cluegroup/ClueGroup.tsx +++ b/frontend/cluesheet-web/src/components/viewer/cluegroup/ClueGroup.tsx @@ -11,11 +11,11 @@ export default function ClueGroup({ clue }: ClueGroupProps) { return ( <>
- +
{clue.children.length > 0 && clue.children.map((clue: Clue, index) => ( - + ))}
diff --git a/frontend/cluesheet-web/src/components/viewer/lineitem/LineItem.tsx b/frontend/cluesheet-web/src/components/viewer/lineitem/LineItem.tsx index 26a87e1..6f55ab2 100644 --- a/frontend/cluesheet-web/src/components/viewer/lineitem/LineItem.tsx +++ b/frontend/cluesheet-web/src/components/viewer/lineitem/LineItem.tsx @@ -12,7 +12,7 @@ export default function LineItem({ clue }: LineItemProps) { // Handle checkbox change const handleClueChecked = () => { - setIsChecked(prevChecked => !prevChecked); + setIsChecked((prevChecked) => !prevChecked); // You can also perform any additional logic here, like updating the clue state }; diff --git a/frontend/cluesheet-web/src/components/viewer/lineitem/page.module.scss b/frontend/cluesheet-web/src/components/viewer/lineitem/page.module.scss index f1030f9..4a912d3 100644 --- a/frontend/cluesheet-web/src/components/viewer/lineitem/page.module.scss +++ b/frontend/cluesheet-web/src/components/viewer/lineitem/page.module.scss @@ -5,35 +5,35 @@ /* Hide the default checkbox */ .customCheckboxInput { - display: none; + display: none; } /* Create a custom checkbox */ .customCheckboxInput { - width: 24px; /* Size of the checkbox */ - height: 24px; /* Size of the checkbox */ - border: 2px solid #ccc; /* Border color */ - display: inline-block; - position: relative; - cursor: pointer; + width: 24px; /* Size of the checkbox */ + height: 24px; /* Size of the checkbox */ + border: 2px solid #ccc; /* Border color */ + display: inline-block; + position: relative; + cursor: pointer; + margin: 0 10px 0 0; } - /* Add a checkmark */ .customCheckboxInput::after { - content: ''; - position: absolute; - left: 6px; /* Position of the checkmark */ - top: 2px; /* Position of the checkmark */ - width: 6px; /* Width of the checkmark */ - height: 12px; /* Height of the checkmark */ - border: solid white; /* Checkmark color */ - border-width: 0 2px 2px 0; /* Checkmark shape */ - transform: rotate(45deg); - opacity: 0; /* Initially hidden */ + content: ""; + position: absolute; + left: 6px; /* Position of the checkmark */ + top: 2px; /* Position of the checkmark */ + width: 6px; /* Width of the checkmark */ + height: 12px; /* Height of the checkmark */ + border: solid white; /* Checkmark color */ + border-width: 0 2px 2px 0; /* Checkmark shape */ + transform: rotate(45deg); + opacity: 0; /* Initially hidden */ } /* Show the checkmark when checked */ .customCheckboxInput:checked + .customCheckbox::after { - opacity: 1; /* Show checkmark */ + opacity: 1; /* Show checkmark */ } diff --git a/frontend/cluesheet-web/src/lib/types.ts b/frontend/cluesheet-web/src/lib/types.ts index 39756f2..498e6e2 100644 --- a/frontend/cluesheet-web/src/lib/types.ts +++ b/frontend/cluesheet-web/src/lib/types.ts @@ -1,8 +1,14 @@ export type Clue = { id: string; - limit: number; - completions: number; + completions: number; // Number of times this has been completed description: string; points: string; + rule: Rule | null; + tags: Array; children: Array; }; + +export type Rule = { + key: string; + description: string; +}; diff --git a/frontend/cluesheet-web/tests/lib/data.ts b/frontend/cluesheet-web/tests/lib/data.ts index 5b0c115..1ef6235 100644 --- a/frontend/cluesheet-web/tests/lib/data.ts +++ b/frontend/cluesheet-web/tests/lib/data.ts @@ -1 +1,77 @@ export const mockCluesheetUUID = "3a4d3998-71cb-42ca-942c-202db9e465c4"; + +export const mockCluesheet = { + id: "my-epic-cluesheet-uuid", + title: "Opcommathon 2069 Cluesheet '>w<'", + user_points: 69420, + user_additional_score: ["-Your Bones", "+Adam Neulight's Car"], + /*point_unit: "Kubernetes Clusters",*/ + clues: [ + { + id: "my-sick-and-poggers-uuid", + limit: 1, + completions: 1, + description: "Eat a whole can of beans", + points: "+5", + rule: null, + tags: [], + children: [ + { + id: "my-sick-and-poggers-uuid-2", + limit: 1, + completions: 0, + description: "No utensils", + points: "+1", + rule: null, + tags: [], + children: [ + { + id: "my-sick-and-poggers-uuid-3", + limit: 1, + completions: 0, + description: "With a straw", + points: "+1", + rule: null, + tags: [], + children: [ + { + id: "my-sick-and-poggers-uuid-4", + limit: 1, + completions: 0, + description: "Is a straw a utensil?", + points: "+1", + + rule: null, + tags: [], + children: [], + }, + ], + }, + ], + }, + { + id: "my-sick-and-poggers-uuid-5", + limit: 1, + completions: 0, + description: "On a bike", + points: "+1", + + rule: null, + tags: [], + children: [], + }, + ], + }, + { + id: "my-sick-and-poggers-uuid-6", + limit: 0, + completions: 3, + description: "Fix something broken (stacks)", + points: "+10", + + rule: null, + tags: [], + children: [], + }, + ], +}; From 4dae755528672949b2908d561b339982f4f73351 Mon Sep 17 00:00:00 2001 From: Will Nilges Date: Sun, 4 May 2025 19:56:01 -0400 Subject: [PATCH 09/12] update cluesheet viewer and checklist --- .../src/app/cluesheet/[cluesheet_id]/page.tsx | 61 ++-------------- .../[cluesheet_id]/user/[user_id]/page.tsx | 16 +++-- .../components/viewer/lineitem/LineItem.tsx | 7 +- frontend/cluesheet-web/src/lib/types.ts | 7 +- frontend/cluesheet-web/tests/lib/data.ts | 72 ++++++++++++++++--- 5 files changed, 85 insertions(+), 78 deletions(-) diff --git a/frontend/cluesheet-web/src/app/cluesheet/[cluesheet_id]/page.tsx b/frontend/cluesheet-web/src/app/cluesheet/[cluesheet_id]/page.tsx index 45f4d38..08d6e25 100644 --- a/frontend/cluesheet-web/src/app/cluesheet/[cluesheet_id]/page.tsx +++ b/frontend/cluesheet-web/src/app/cluesheet/[cluesheet_id]/page.tsx @@ -5,6 +5,7 @@ import FlipNumbers from "react-flip-numbers"; import PointCounter from "@/components/viewer/counter/PointCounter"; import { Clue } from "@/lib/types"; import ClueGroup from "@/components/viewer/cluegroup/ClueGroup"; +import { mockCluesheet } from "../../../../tests/lib/data"; export const metadata = { title: "View cluesheet", @@ -19,58 +20,8 @@ export default async function CluesheetViewer({ //const { cluesheet_id } = await params; //const cluesheet = await fetch(`${cluesheetBackendEndpoint}/api/v1/cluesheet/${cluesheet_id}`); //console.log(`Got cluesheet object: ${await cluesheet.json()}`); - - const cluesheet = { - id: "my-epic-cluesheet-uuid", - title: "Opcommathon 2069 Cluesheet '>w<'", - clues: [ - { - id: "my-sick-and-poggers-uuid", - limit: 1, - description: "Eat a whole can of beans", - points: "+5", - children: [ - { - id: "my-sick-and-poggers-uuid-2", - limit: 1, - description: "No utensils", - points: "+1", - children: [ - { - id: "my-sick-and-poggers-uuid-3", - limit: 1, - description: "With a straw", - points: "+1", - children: [ - { - id: "my-sick-and-poggers-uuid-4", - limit: 1, - description: "Is a straw a utensil?", - points: "+1", - children: [], - }, - ], - }, - ], - }, - { - id: "my-sick-and-poggers-uuid-5", - limit: 1, - description: "On a bike", - points: "+1", - children: [], - }, - ], - }, - { - id: "my-sick-and-poggers-uuid-6", - limit: 0, - description: "Fix something broken (stacks)", - points: "+10", - children: [], - }, - ], - }; + + const cluesheet = mockCluesheet; return ( <> @@ -78,15 +29,11 @@ export default async function CluesheetViewer({

{cluesheet.title}

-
- -

Points

-
{cluesheet.clues.length > 0 && cluesheet.clues.map((clue: Clue, index) => ( - + ))}
diff --git a/frontend/cluesheet-web/src/app/cluesheet/[cluesheet_id]/user/[user_id]/page.tsx b/frontend/cluesheet-web/src/app/cluesheet/[cluesheet_id]/user/[user_id]/page.tsx index 515040f..1e369a3 100644 --- a/frontend/cluesheet-web/src/app/cluesheet/[cluesheet_id]/user/[user_id]/page.tsx +++ b/frontend/cluesheet-web/src/app/cluesheet/[cluesheet_id]/user/[user_id]/page.tsx @@ -3,9 +3,9 @@ import styles from "./page.module.scss"; import { Container } from "reactstrap"; import FlipNumbers from "react-flip-numbers"; import PointCounter from "@/components/viewer/counter/PointCounter"; -import { Clue } from "@/lib/types"; +import { Clue, UserClue } from "@/lib/types"; import ClueGroup from "@/components/viewer/cluegroup/ClueGroup"; -import { mockCluesheet } from "../../../../../../tests/lib/data"; +import { mockUserCluesheet } from "../../../../../../tests/lib/data"; export const metadata = { title: "View cluesheet", @@ -18,23 +18,25 @@ export default async function CluesheetEditor({ params: Promise<{ cluesheet_id: string; keycloak_uid: string }>; }) { //const { cluesheet_id } = await params; - //const cluesheet = await fetch(`${cluesheetBackendEndpoint}/api/v1/cluesheet/${cluesheet_id}`); + //const cluesheet = await fetch(`${cluesheetBackendEndpoint}/api/v1/cluesheet/${cluesheet_id}/user/${keycloak_uid}`); //console.log(`Got cluesheet object: ${await cluesheet.json()}`); + + const cluesheet = mockUserCluesheet; return ( <>
-

{mockCluesheet.title}

+

{cluesheet.title}

- +

Points

- {mockCluesheet.clues.length > 0 && - mockCluesheet.clues.map((clue: Clue) => ( + {cluesheet.clues.length > 0 && + cluesheet.clues.map((clue: UserClue) => ( ))}
diff --git a/frontend/cluesheet-web/src/components/viewer/lineitem/LineItem.tsx b/frontend/cluesheet-web/src/components/viewer/lineitem/LineItem.tsx index 6f55ab2..2da4623 100644 --- a/frontend/cluesheet-web/src/components/viewer/lineitem/LineItem.tsx +++ b/frontend/cluesheet-web/src/components/viewer/lineitem/LineItem.tsx @@ -1,4 +1,4 @@ -import { Clue } from "@/lib/types"; +import { Clue, UserClue } from "@/lib/types"; import styles from "./page.module.scss"; import { useState } from "react"; @@ -8,7 +8,7 @@ interface LineItemProps { export default function LineItem({ clue }: LineItemProps) { // Initialize state to track if the checkbox is checked - const [isChecked, setIsChecked] = useState(Number(clue.completions) > 0); + const [isChecked, setIsChecked] = useState((clue as UserClue).completions !== undefined && Number((clue as UserClue).completions) > 0); // Handle checkbox change const handleClueChecked = () => { @@ -19,12 +19,13 @@ export default function LineItem({ clue }: LineItemProps) { return ( <>
+ { (clue as UserClue).completions !== undefined && + />}
{clue.points}: {clue.description}
diff --git a/frontend/cluesheet-web/src/lib/types.ts b/frontend/cluesheet-web/src/lib/types.ts index 498e6e2..055038d 100644 --- a/frontend/cluesheet-web/src/lib/types.ts +++ b/frontend/cluesheet-web/src/lib/types.ts @@ -1,6 +1,5 @@ -export type Clue = { +export interface Clue { id: string; - completions: number; // Number of times this has been completed description: string; points: string; rule: Rule | null; @@ -8,6 +7,10 @@ export type Clue = { children: Array; }; +export interface UserClue extends Clue { + completions: number; // Number of times this has been completed +} + export type Rule = { key: string; description: string; diff --git a/frontend/cluesheet-web/tests/lib/data.ts b/frontend/cluesheet-web/tests/lib/data.ts index 1ef6235..e13fbb4 100644 --- a/frontend/cluesheet-web/tests/lib/data.ts +++ b/frontend/cluesheet-web/tests/lib/data.ts @@ -9,7 +9,69 @@ export const mockCluesheet = { clues: [ { id: "my-sick-and-poggers-uuid", - limit: 1, + description: "Eat a whole can of beans", + points: "+5", + rule: null, + tags: [], + children: [ + { + id: "my-sick-and-poggers-uuid-2", + description: "No utensils", + points: "+1", + rule: null, + tags: [], + children: [ + { + id: "my-sick-and-poggers-uuid-3", + description: "With a straw", + points: "+1", + rule: null, + tags: [], + children: [ + { + id: "my-sick-and-poggers-uuid-4", + description: "Is a straw a utensil?", + points: "+1", + + rule: null, + tags: [], + children: [], + }, + ], + }, + ], + }, + { + id: "my-sick-and-poggers-uuid-5", + description: "On a bike", + points: "+1", + + rule: null, + tags: [], + children: [], + }, + ], + }, + { + id: "my-sick-and-poggers-uuid-6", + description: "Fix something broken (stacks)", + points: "+10", + rule: null, + tags: [], + children: [], + }, + ], +}; + +export const mockUserCluesheet = { + id: "my-epic-cluesheet-uuid", + title: "Opcommathon 2069 Cluesheet '>w<'", + user_points: 69420, + user_additional_score: ["-Your Bones", "+Adam Neulight's Car"], + /*point_unit: "Kubernetes Clusters",*/ + clues: [ + { + id: "my-sick-and-poggers-uuid", completions: 1, description: "Eat a whole can of beans", points: "+5", @@ -18,7 +80,6 @@ export const mockCluesheet = { children: [ { id: "my-sick-and-poggers-uuid-2", - limit: 1, completions: 0, description: "No utensils", points: "+1", @@ -27,7 +88,6 @@ export const mockCluesheet = { children: [ { id: "my-sick-and-poggers-uuid-3", - limit: 1, completions: 0, description: "With a straw", points: "+1", @@ -36,11 +96,9 @@ export const mockCluesheet = { children: [ { id: "my-sick-and-poggers-uuid-4", - limit: 1, completions: 0, description: "Is a straw a utensil?", points: "+1", - rule: null, tags: [], children: [], @@ -51,11 +109,9 @@ export const mockCluesheet = { }, { id: "my-sick-and-poggers-uuid-5", - limit: 1, completions: 0, description: "On a bike", points: "+1", - rule: null, tags: [], children: [], @@ -64,11 +120,9 @@ export const mockCluesheet = { }, { id: "my-sick-and-poggers-uuid-6", - limit: 0, completions: 3, description: "Fix something broken (stacks)", points: "+10", - rule: null, tags: [], children: [], From 910ba904f9b3e90dcb6a3014b728fc63e6e5b68f Mon Sep 17 00:00:00 2001 From: Will Nilges Date: Sun, 4 May 2025 20:55:13 -0400 Subject: [PATCH 10/12] Tags and maybe some other stuff --- .../src/app/cluesheet/[cluesheet_id]/page.tsx | 2 +- .../[cluesheet_id]/user/[user_id]/page.tsx | 2 +- .../cluesheet-web/src/app/page.module.css | 168 ------------------ .../components/viewer/cluegroup/ClueGroup.tsx | 10 +- .../viewer/cluegroup/page.module.scss | 4 + .../components/viewer/lineitem/LineItem.tsx | 36 ++-- .../viewer/lineitem/page.module.scss | 22 +++ frontend/cluesheet-web/src/lib/types.ts | 2 +- frontend/cluesheet-web/tests/lib/data.ts | 77 ++++---- 9 files changed, 96 insertions(+), 227 deletions(-) delete mode 100644 frontend/cluesheet-web/src/app/page.module.css diff --git a/frontend/cluesheet-web/src/app/cluesheet/[cluesheet_id]/page.tsx b/frontend/cluesheet-web/src/app/cluesheet/[cluesheet_id]/page.tsx index 08d6e25..00ba48d 100644 --- a/frontend/cluesheet-web/src/app/cluesheet/[cluesheet_id]/page.tsx +++ b/frontend/cluesheet-web/src/app/cluesheet/[cluesheet_id]/page.tsx @@ -20,7 +20,7 @@ export default async function CluesheetViewer({ //const { cluesheet_id } = await params; //const cluesheet = await fetch(`${cluesheetBackendEndpoint}/api/v1/cluesheet/${cluesheet_id}`); //console.log(`Got cluesheet object: ${await cluesheet.json()}`); - + const cluesheet = mockCluesheet; return ( diff --git a/frontend/cluesheet-web/src/app/cluesheet/[cluesheet_id]/user/[user_id]/page.tsx b/frontend/cluesheet-web/src/app/cluesheet/[cluesheet_id]/user/[user_id]/page.tsx index 1e369a3..41b69d3 100644 --- a/frontend/cluesheet-web/src/app/cluesheet/[cluesheet_id]/user/[user_id]/page.tsx +++ b/frontend/cluesheet-web/src/app/cluesheet/[cluesheet_id]/user/[user_id]/page.tsx @@ -20,7 +20,7 @@ export default async function CluesheetEditor({ //const { cluesheet_id } = await params; //const cluesheet = await fetch(`${cluesheetBackendEndpoint}/api/v1/cluesheet/${cluesheet_id}/user/${keycloak_uid}`); //console.log(`Got cluesheet object: ${await cluesheet.json()}`); - + const cluesheet = mockUserCluesheet; return ( diff --git a/frontend/cluesheet-web/src/app/page.module.css b/frontend/cluesheet-web/src/app/page.module.css deleted file mode 100644 index a11c8f3..0000000 --- a/frontend/cluesheet-web/src/app/page.module.css +++ /dev/null @@ -1,168 +0,0 @@ -.page { - --gray-rgb: 0, 0, 0; - --gray-alpha-200: rgba(var(--gray-rgb), 0.08); - --gray-alpha-100: rgba(var(--gray-rgb), 0.05); - - --button-primary-hover: #383838; - --button-secondary-hover: #f2f2f2; - - display: grid; - grid-template-rows: 20px 1fr 20px; - align-items: center; - justify-items: center; - min-height: 100svh; - padding: 80px; - gap: 64px; - font-family: var(--font-geist-sans); -} - -@media (prefers-color-scheme: dark) { - .page { - --gray-rgb: 255, 255, 255; - --gray-alpha-200: rgba(var(--gray-rgb), 0.145); - --gray-alpha-100: rgba(var(--gray-rgb), 0.06); - - --button-primary-hover: #ccc; - --button-secondary-hover: #1a1a1a; - } -} - -.main { - display: flex; - flex-direction: column; - gap: 32px; - grid-row-start: 2; -} - -.main ol { - font-family: var(--font-geist-mono); - padding-left: 0; - margin: 0; - font-size: 14px; - line-height: 24px; - letter-spacing: -0.01em; - list-style-position: inside; -} - -.main li:not(:last-of-type) { - margin-bottom: 8px; -} - -.main code { - font-family: inherit; - background: var(--gray-alpha-100); - padding: 2px 4px; - border-radius: 4px; - font-weight: 600; -} - -.ctas { - display: flex; - gap: 16px; -} - -.ctas a { - appearance: none; - border-radius: 128px; - height: 48px; - padding: 0 20px; - border: none; - border: 1px solid transparent; - transition: - background 0.2s, - color 0.2s, - border-color 0.2s; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - font-size: 16px; - line-height: 20px; - font-weight: 500; -} - -a.primary { - background: var(--foreground); - color: var(--background); - gap: 8px; -} - -a.secondary { - border-color: var(--gray-alpha-200); - min-width: 158px; -} - -.footer { - grid-row-start: 3; - display: flex; - gap: 24px; -} - -.footer a { - display: flex; - align-items: center; - gap: 8px; -} - -.footer img { - flex-shrink: 0; -} - -/* Enable hover only on non-touch devices */ -@media (hover: hover) and (pointer: fine) { - a.primary:hover { - background: var(--button-primary-hover); - border-color: transparent; - } - - a.secondary:hover { - background: var(--button-secondary-hover); - border-color: transparent; - } - - .footer a:hover { - text-decoration: underline; - text-underline-offset: 4px; - } -} - -@media (max-width: 600px) { - .page { - padding: 32px; - padding-bottom: 80px; - } - - .main { - align-items: center; - } - - .main ol { - text-align: center; - } - - .ctas { - flex-direction: column; - } - - .ctas a { - font-size: 14px; - height: 40px; - padding: 0 16px; - } - - a.secondary { - min-width: auto; - } - - .footer { - flex-wrap: wrap; - align-items: center; - justify-content: center; - } -} - -@media (prefers-color-scheme: dark) { - .logo { - filter: invert(); - } -} diff --git a/frontend/cluesheet-web/src/components/viewer/cluegroup/ClueGroup.tsx b/frontend/cluesheet-web/src/components/viewer/cluegroup/ClueGroup.tsx index ae5edb5..492baf6 100644 --- a/frontend/cluesheet-web/src/components/viewer/cluegroup/ClueGroup.tsx +++ b/frontend/cluesheet-web/src/components/viewer/cluegroup/ClueGroup.tsx @@ -1,21 +1,21 @@ "use client"; -import { Clue } from "@/lib/types"; +import { Clue, UserClue } from "@/lib/types"; import styles from "./page.module.scss"; import LineItem from "../lineitem/LineItem"; interface ClueGroupProps { - clue: Clue; + clue: Clue | UserClue; } export default function ClueGroup({ clue }: ClueGroupProps) { return ( <> -
- +
+
{clue.children.length > 0 && clue.children.map((clue: Clue, index) => ( - + ))}
diff --git a/frontend/cluesheet-web/src/components/viewer/cluegroup/page.module.scss b/frontend/cluesheet-web/src/components/viewer/cluegroup/page.module.scss index f611f1e..f90f1e5 100644 --- a/frontend/cluesheet-web/src/components/viewer/cluegroup/page.module.scss +++ b/frontend/cluesheet-web/src/components/viewer/cluegroup/page.module.scss @@ -9,6 +9,10 @@ border-radius: 8px; /* Optional rounded corners */ } +.locked { + background-color: #ff5555; +} + .children { margin-left: 20px; } diff --git a/frontend/cluesheet-web/src/components/viewer/lineitem/LineItem.tsx b/frontend/cluesheet-web/src/components/viewer/lineitem/LineItem.tsx index 2da4623..ebf350c 100644 --- a/frontend/cluesheet-web/src/components/viewer/lineitem/LineItem.tsx +++ b/frontend/cluesheet-web/src/components/viewer/lineitem/LineItem.tsx @@ -3,32 +3,46 @@ import { Clue, UserClue } from "@/lib/types"; import styles from "./page.module.scss"; import { useState } from "react"; interface LineItemProps { - clue: Clue; + clue: Clue | UserClue; } export default function LineItem({ clue }: LineItemProps) { // Initialize state to track if the checkbox is checked - const [isChecked, setIsChecked] = useState((clue as UserClue).completions !== undefined && Number((clue as UserClue).completions) > 0); + const [isChecked, setIsChecked] = useState( + (clue as UserClue).completions !== undefined && + Number((clue as UserClue).completions) > 0, + ); // Handle checkbox change const handleClueChecked = () => { setIsChecked((prevChecked) => !prevChecked); + (clue as UserClue).completions = 1; // You can also perform any additional logic here, like updating the clue state }; return ( <>
- { (clue as UserClue).completions !== undefined && - } -
- {clue.points}: {clue.description} + {(clue as UserClue).completions !== undefined && ( + + )} +
+ {clue.points}: +
+
+ {clue.description}
+
+ {clue.tags.length > 0 && + clue.tags.map((tag: string, index) => ( +

{tag}

+ ))} +
); diff --git a/frontend/cluesheet-web/src/components/viewer/lineitem/page.module.scss b/frontend/cluesheet-web/src/components/viewer/lineitem/page.module.scss index 4a912d3..da63e12 100644 --- a/frontend/cluesheet-web/src/components/viewer/lineitem/page.module.scss +++ b/frontend/cluesheet-web/src/components/viewer/lineitem/page.module.scss @@ -3,6 +3,10 @@ flex-direction: row; } +.lineItem h5 { + margin: 0px; +} + /* Hide the default checkbox */ .customCheckboxInput { display: none; @@ -37,3 +41,21 @@ .customCheckboxInput:checked + .customCheckbox::after { opacity: 1; /* Show checkmark */ } + +.description { + font-weight: 400; +} + +.tags { + display:flex; + flex: 1; + flex-direction: row; + justify-content: right; +} + +.tag { + border: 1px solid #ccc; /* Border color */ + border-radius: 5px; + padding: 0 2px; + margin: 0 5px; +} diff --git a/frontend/cluesheet-web/src/lib/types.ts b/frontend/cluesheet-web/src/lib/types.ts index 055038d..3152650 100644 --- a/frontend/cluesheet-web/src/lib/types.ts +++ b/frontend/cluesheet-web/src/lib/types.ts @@ -5,7 +5,7 @@ export interface Clue { rule: Rule | null; tags: Array; children: Array; -}; +} export interface UserClue extends Clue { completions: number; // Number of times this has been completed diff --git a/frontend/cluesheet-web/tests/lib/data.ts b/frontend/cluesheet-web/tests/lib/data.ts index e13fbb4..672c6b0 100644 --- a/frontend/cluesheet-web/tests/lib/data.ts +++ b/frontend/cluesheet-web/tests/lib/data.ts @@ -2,62 +2,59 @@ export const mockCluesheetUUID = "3a4d3998-71cb-42ca-942c-202db9e465c4"; export const mockCluesheet = { id: "my-epic-cluesheet-uuid", - title: "Opcommathon 2069 Cluesheet '>w<'", - user_points: 69420, - user_additional_score: ["-Your Bones", "+Adam Neulight's Car"], - /*point_unit: "Kubernetes Clusters",*/ + title: "Yet-another-Rip n' Dip-a-thon 2192", clues: [ { id: "my-sick-and-poggers-uuid", - description: "Eat a whole can of beans", - points: "+5", + description: "Linux", + points: "+1", rule: null, - tags: [], + tags: ["Operating Systems", "Computers", "FOSS"], children: [ { - id: "my-sick-and-poggers-uuid-2", - description: "No utensils", - points: "+1", + id: "my-sick-and-poggers-uuid-100", + description: "Arch", + points: "+2", rule: null, tags: [], - children: [ - { - id: "my-sick-and-poggers-uuid-3", - description: "With a straw", - points: "+1", - rule: null, - tags: [], - children: [ - { - id: "my-sick-and-poggers-uuid-4", - description: "Is a straw a utensil?", - points: "+1", - - rule: null, - tags: [], - children: [], - }, - ], - }, - ], + children: [], }, { - id: "my-sick-and-poggers-uuid-5", - description: "On a bike", + id: "my-sick-and-poggers-uuid-101", + description: "Install Linux", points: "+1", - - rule: null, + rule: { + key: "stacks", + description: + "You can complete this rule as many times as you want.", + }, tags: [], - children: [], + children: [ + { + id: "my-sick-and-poggers-uuid-100", + description: "Not your computer", + points: "+4", + rule: { + key: "stacks", + description: + "You can complete this rule as many times as you want.", + }, + tags: ["Operating Systems", "Computers", "FOSS"], + children: [], + }, + ], }, ], }, { id: "my-sick-and-poggers-uuid-6", - description: "Fix something broken (stacks)", + description: "Fix something broken", points: "+10", - rule: null, - tags: [], + rule: { + key: "stacks", + description: "You can complete this clue as many times as you want.", + }, + tags: ["Opcommathon 2023"], children: [], }, ], @@ -76,7 +73,7 @@ export const mockUserCluesheet = { description: "Eat a whole can of beans", points: "+5", rule: null, - tags: [], + tags: ["beans"], children: [ { id: "my-sick-and-poggers-uuid-2", @@ -100,7 +97,7 @@ export const mockUserCluesheet = { description: "Is a straw a utensil?", points: "+1", rule: null, - tags: [], + tags: ["beans"], children: [], }, ], From 01ca9fb4325a2d738ea88f7562eb01c5439c7f03 Mon Sep 17 00:00:00 2001 From: Will Nilges Date: Sun, 4 May 2025 22:05:11 -0400 Subject: [PATCH 11/12] add a bit of padding --- .../src/components/viewer/lineitem/page.module.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/frontend/cluesheet-web/src/components/viewer/lineitem/page.module.scss b/frontend/cluesheet-web/src/components/viewer/lineitem/page.module.scss index da63e12..35ce2de 100644 --- a/frontend/cluesheet-web/src/components/viewer/lineitem/page.module.scss +++ b/frontend/cluesheet-web/src/components/viewer/lineitem/page.module.scss @@ -7,6 +7,10 @@ margin: 0px; } +.points { + padding: 0 5px; +} + /* Hide the default checkbox */ .customCheckboxInput { display: none; From 65b15ac49258d518741bafcfe3f77b48844505ff Mon Sep 17 00:00:00 2001 From: Will Nilges Date: Sun, 4 May 2025 22:36:25 -0400 Subject: [PATCH 12/12] kludge in parent --- .../src/app/cluesheet/[cluesheet_id]/page.tsx | 2 +- .../[cluesheet_id]/user/[user_id]/page.tsx | 2 +- .../components/viewer/cluegroup/ClueGroup.tsx | 17 +++++++++++++---- .../components/viewer/lineitem/LineItem.tsx | 18 ++++++++++++++---- .../viewer/lineitem/page.module.scss | 4 ++++ 5 files changed, 33 insertions(+), 10 deletions(-) diff --git a/frontend/cluesheet-web/src/app/cluesheet/[cluesheet_id]/page.tsx b/frontend/cluesheet-web/src/app/cluesheet/[cluesheet_id]/page.tsx index 00ba48d..ddb1ce4 100644 --- a/frontend/cluesheet-web/src/app/cluesheet/[cluesheet_id]/page.tsx +++ b/frontend/cluesheet-web/src/app/cluesheet/[cluesheet_id]/page.tsx @@ -33,7 +33,7 @@ export default async function CluesheetViewer({
{cluesheet.clues.length > 0 && cluesheet.clues.map((clue: Clue, index) => ( - + ))}
diff --git a/frontend/cluesheet-web/src/app/cluesheet/[cluesheet_id]/user/[user_id]/page.tsx b/frontend/cluesheet-web/src/app/cluesheet/[cluesheet_id]/user/[user_id]/page.tsx index 41b69d3..811c04a 100644 --- a/frontend/cluesheet-web/src/app/cluesheet/[cluesheet_id]/user/[user_id]/page.tsx +++ b/frontend/cluesheet-web/src/app/cluesheet/[cluesheet_id]/user/[user_id]/page.tsx @@ -37,7 +37,7 @@ export default async function CluesheetEditor({
{cluesheet.clues.length > 0 && cluesheet.clues.map((clue: UserClue) => ( - + ))}
diff --git a/frontend/cluesheet-web/src/components/viewer/cluegroup/ClueGroup.tsx b/frontend/cluesheet-web/src/components/viewer/cluegroup/ClueGroup.tsx index 492baf6..0eca931 100644 --- a/frontend/cluesheet-web/src/components/viewer/cluegroup/ClueGroup.tsx +++ b/frontend/cluesheet-web/src/components/viewer/cluegroup/ClueGroup.tsx @@ -4,18 +4,27 @@ import styles from "./page.module.scss"; import LineItem from "../lineitem/LineItem"; interface ClueGroupProps { + parent: Clue | UserClue | null; clue: Clue | UserClue; } -export default function ClueGroup({ clue }: ClueGroupProps) { +export default function ClueGroup({ parent, clue }: ClueGroupProps) { + if (parent !== null) { + console.log(`OG Tags: ${parent.tags}`); + parent.tags = parent?.tags.concat(clue.tags); + console.log(`New Tags: ${parent.tags}`); + } else { + console.log(`Error: parent is ${parent}`); + } + return ( <>
- +
{clue.children.length > 0 && - clue.children.map((clue: Clue, index) => ( - + clue.children.map((child: Clue, index) => ( + ))}
diff --git a/frontend/cluesheet-web/src/components/viewer/lineitem/LineItem.tsx b/frontend/cluesheet-web/src/components/viewer/lineitem/LineItem.tsx index ebf350c..81e5cb9 100644 --- a/frontend/cluesheet-web/src/components/viewer/lineitem/LineItem.tsx +++ b/frontend/cluesheet-web/src/components/viewer/lineitem/LineItem.tsx @@ -3,10 +3,11 @@ import { Clue, UserClue } from "@/lib/types"; import styles from "./page.module.scss"; import { useState } from "react"; interface LineItemProps { + parent: Clue | UserClue | null; clue: Clue | UserClue; } -export default function LineItem({ clue }: LineItemProps) { +export default function LineItem({ parent, clue }: LineItemProps) { // Initialize state to track if the checkbox is checked const [isChecked, setIsChecked] = useState( (clue as UserClue).completions !== undefined && @@ -20,9 +21,18 @@ export default function LineItem({ clue }: LineItemProps) { // You can also perform any additional logic here, like updating the clue state }; + const shouldLock = (parent !== null && (parent as UserClue).completions <= 0); + + let tags = []; + if (parent != null) { + tags = clue.tags.filter(item => !parent.tags.includes(item)); + } else { + tags = clue.tags; + } + return ( <> -
+
{(clue as UserClue).completions !== undefined && (
- {clue.tags.length > 0 && - clue.tags.map((tag: string, index) => ( + {tags.length > 0 && + tags.map((tag: string, index) => (

{tag}

))}
diff --git a/frontend/cluesheet-web/src/components/viewer/lineitem/page.module.scss b/frontend/cluesheet-web/src/components/viewer/lineitem/page.module.scss index 35ce2de..0caa775 100644 --- a/frontend/cluesheet-web/src/components/viewer/lineitem/page.module.scss +++ b/frontend/cluesheet-web/src/components/viewer/lineitem/page.module.scss @@ -7,6 +7,10 @@ margin: 0px; } +.locked { + background-color: #ff6666; +} + .points { padding: 0 5px; }