├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── .stylelintrc.js ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── README.md ├── docker-compose.yml ├── firebase.json ├── package.json ├── public ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── apple-touch-icon.png ├── audio │ ├── button.mp3 │ ├── cancel.mp3 │ ├── collapse.mp3 │ ├── correct.mp3 │ ├── end.mp3 │ ├── hint.mp3 │ ├── incorrect.mp3 │ ├── music.mp3 │ ├── offline.mp3 │ ├── online.mp3 │ ├── question.mp3 │ ├── start.mp3 │ └── uncollapse.mp3 ├── browserconfig.xml ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── index.html ├── mstile-150x150.png ├── ogp.png ├── safari-pinned-tab.svg └── site.webmanifest ├── src ├── App.css ├── App.test.tsx ├── App.tsx ├── assets │ ├── data │ │ ├── attractions.tsx │ │ ├── capes.tsx │ │ ├── castles.tsx │ │ ├── castles_archive.json │ │ ├── cities.tsx │ │ ├── cuisines.tsx │ │ ├── festivals.tsx │ │ ├── goods.tsx │ │ ├── lakes_archive.txt │ │ ├── mountains.tsx │ │ ├── museums.tsx │ │ ├── powerplants.tsx │ │ ├── prefecture.tsx │ │ ├── quiz.tsx │ │ ├── reststops.tsx │ │ ├── spas.tsx │ │ ├── specialties.tsx │ │ ├── stations.tsx │ │ ├── sweets.tsx │ │ └── 作業用.json │ ├── icon │ │ └── spa.tsx │ └── svg │ │ ├── home-bg.svg │ │ ├── logo-horizontal.svg │ │ ├── logo-vertical.svg │ │ └── sp-bg.svg ├── components │ ├── atoms │ │ └── deviceicon.tsx │ ├── pages │ │ └── privacy.tsx │ └── templates │ │ └── prizmfooter.tsx ├── ducks │ ├── game.ts │ ├── rootReducer.ts │ └── user.ts ├── hooks │ ├── use-audio.ts │ ├── use-generates.ts │ ├── use-getdevice.ts │ ├── use-listengameanddeleteuser.ts │ ├── use-scrolldiv.ts │ ├── use-toonline.ts │ └── use-username.ts ├── index.tsx ├── modules │ ├── edituser │ │ ├── edituser.colorselector.tsx │ │ ├── edituser.tsx │ │ ├── edituser.userpreview.tsx │ │ ├── usersummary.item.tsx │ │ └── usersummary.tsx │ ├── game │ │ ├── answerinput │ │ │ ├── answerinput.tsx │ │ │ ├── inputerrormessage.tsx │ │ │ ├── inputsuggest.tsx │ │ │ ├── use-canonicalizepref.ts │ │ │ ├── use-inputerrormessage.ts │ │ │ ├── use-judger.ts │ │ │ ├── use-userscore.ts │ │ │ └── userremain.tsx │ │ ├── chat │ │ │ ├── chatcontainer.chat.tsx │ │ │ ├── chatcontainer.matchedtext.tsx │ │ │ ├── chatcontainer.tsx │ │ │ ├── message.content.scores.notice.tsx │ │ │ ├── message.content.scores.tsx │ │ │ ├── message.content.tsx │ │ │ ├── message.tsx │ │ │ └── use-matchedanimation.ts │ │ ├── game.tsx │ │ ├── questioner │ │ │ ├── answerdisplay.pref3d.svg.tsx │ │ │ ├── answerdisplay.pref3d.tsx │ │ │ ├── answerdisplay.tsx │ │ │ ├── bigquestion.circle.tsx │ │ │ ├── bigquestion.tsx │ │ │ ├── gamecancelonready.tsx │ │ │ ├── gameready.tsx │ │ │ ├── questioner.tsx │ │ │ ├── questionlist.tsx │ │ │ ├── questionlistcontainer.tsx │ │ │ ├── use-censorship.ts │ │ │ ├── use-fitfontsizetowidth.ts │ │ │ └── use-questiontimer.ts │ │ ├── use-finishgame.ts │ │ ├── use-fitScreenHeight.ts │ │ └── use-gamestarted.ts │ └── home │ │ ├── gamesetter.modeselector.tsx │ │ ├── gamesetter.tsx │ │ ├── home.logo.tsx │ │ ├── home.tsx │ │ ├── onlineusers.header.tsx │ │ ├── onlineusers.tsx │ │ ├── onlineusers.userlist.tsx │ │ ├── use-pushgame.ts │ │ └── use-usermounted.ts ├── react-app-env.d.ts ├── reportWebVitals.ts ├── setupTests.ts └── utils │ ├── database.ts │ ├── gethint.ts │ ├── summary.ts │ └── types.tsx ├── tsconfig.eslint.json ├── tsconfig.json └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | build/ 2 | public/ 3 | **/coverage/ 4 | **/node_modules/ 5 | **/*.min.js 6 | *.config.js 7 | .*lintrc.js 8 | src/stories/ 9 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | }, 6 | extends: [ 7 | 'plugin:react/recommended', 8 | 'airbnb', 9 | 'airbnb/hooks', 10 | 'plugin:import/errors', 11 | 'plugin:import/warnings', 12 | 'plugin:import/typescript', 13 | 'plugin:@typescript-eslint/recommended', 14 | 'plugin:@typescript-eslint/recommended-requiring-type-checking', 15 | 'prettier', 16 | ], 17 | parser: '@typescript-eslint/parser', 18 | parserOptions: { 19 | ecmaFeatures: { 20 | jsx: true, 21 | }, 22 | ecmaVersion: 'latest', 23 | project: './tsconfig.eslint.json', 24 | sourceType: 'module', 25 | tsconfigRootDir: __dirname, 26 | }, 27 | plugins: [ 28 | '@typescript-eslint', 29 | 'import', 30 | 'jsx-a11y', 31 | 'prefer-arrow', 32 | 'react', 33 | 'react-hooks', 34 | ], 35 | root: true, 36 | rules: { 37 | // occur error in `import React from 'react'` with react-scripts 4.0.1 38 | 'no-use-before-define': 'off', 39 | '@typescript-eslint/no-use-before-define': ['error'], 40 | 'lines-between-class-members': [ 41 | 'error', 42 | 'always', 43 | { 44 | exceptAfterSingleLine: true, 45 | }, 46 | ], 47 | 'no-void': [ 48 | 'error', 49 | { 50 | allowAsStatement: true, 51 | }, 52 | ], 53 | 'padding-line-between-statements': [ 54 | 'error', 55 | { 56 | blankLine: 'always', 57 | prev: '*', 58 | next: 'return', 59 | }, 60 | ], 61 | '@typescript-eslint/no-unused-vars': [ 62 | 'error', 63 | { 64 | vars: 'all', 65 | args: 'after-used', 66 | argsIgnorePattern: '_', 67 | ignoreRestSiblings: false, 68 | varsIgnorePattern: '_', 69 | }, 70 | ], 71 | 'import/extensions': [ 72 | 'error', 73 | 'ignorePackages', 74 | { 75 | js: 'never', 76 | jsx: 'never', 77 | ts: 'never', 78 | tsx: 'never', 79 | }, 80 | ], 81 | 'react/function-component-definition': [ 82 | 2, 83 | { 84 | namedComponents: 'arrow-function', 85 | }, 86 | ], 87 | 'prefer-arrow/prefer-arrow-functions': [ 88 | 'error', 89 | { 90 | disallowPrototype: true, 91 | singleReturnOnly: false, 92 | classPropertiesAllowed: false, 93 | }, 94 | ], 95 | 'react/jsx-filename-extension': [ 96 | 'error', 97 | { 98 | extensions: ['.jsx', '.tsx'], 99 | }, 100 | ], 101 | 'react/jsx-props-no-spreading': [ 102 | 'error', 103 | { 104 | html: 'enforce', 105 | custom: 'enforce', 106 | explicitSpread: 'ignore', 107 | }, 108 | ], 109 | 'react/react-in-jsx-scope': 'off', 110 | }, 111 | overrides: [ 112 | { 113 | files: ['*.tsx'], 114 | rules: { 115 | 'react/prop-types': 'off', 116 | }, 117 | }, 118 | ], 119 | settings: { 120 | 'import/resolver': { 121 | node: { 122 | paths: ['src'], 123 | moduleDirectory: ['src', 'node_modules'], 124 | }, 125 | }, 126 | }, 127 | }; 128 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | 26 | ### StorybookJs ### 27 | # website: https://storybook.js.org/ 28 | 29 | storybook-static/ 30 | 31 | ### Firebase ### 32 | .idea 33 | **/.firebaserc 34 | .firebaserc 35 | database.rules.json 36 | 37 | ### Firebase Patch ### 38 | .runtimeconfig.json 39 | .firebase/ 40 | 41 | firebase-hosting-merge.yml 42 | firebase-hosting-pull-request.yml 43 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "endOfLine": "auto" 5 | } -------------------------------------------------------------------------------- /.stylelintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['stylelint-config-standard', 'stylelint-config-recess-order'], 3 | plugins: ['stylelint-order'], 4 | ignoreFiles: ['**/node_modules/**', '**/stories/**'], 5 | rules: { 6 | 'string-quotes': 'single', 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. 3 | // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp 4 | 5 | // List of extensions which should be recommended for users of this workspace. 6 | "recommendations": [ 7 | "CoenraadS.bracket-pair-colorizer-2", 8 | "dbaeumer.vscode-eslint", 9 | "donjayamanne.githistory", 10 | "esbenp.prettier-vscode", 11 | "msjsdiag.debugger-for-chrome", 12 | "oderwat.indent-rainbow", 13 | "stylelint.vscode-stylelint", 14 | "VisualStudioExptTeam.vscodeintellicode", 15 | "vscode-icons-team.vscode-icons", 16 | "wix.vscode-import-cost" 17 | ], 18 | // List of extensions recommended by VS Code that should not be recommended for users of this workspace. 19 | "unwantedRecommendations": [] 20 | } 21 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // IntelliSense を使用して利用可能な属性を学べます。 3 | // 既存の属性の説明をホバーして表示します。 4 | // 詳細情報は次を確認してください: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "pwa-chrome", 9 | "request": "launch", 10 | "name": "Launch Chrome against localhost", 11 | "url": "http://localhost:3000", 12 | "webRoot": "${workspaceFolder}" 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "css.validate": false, 3 | "less.validate": false, 4 | "scss.validate": false, 5 | "editor.codeActionsOnSave": { 6 | "source.fixAll.eslint": true, 7 | "source.fixAll.stylelint": true 8 | }, 9 | "editor.defaultFormatter": "esbenp.prettier-vscode", 10 | "editor.formatOnSave": false, 11 | "[graphql]": { 12 | "editor.formatOnSave": true 13 | }, 14 | "[javascript]": { 15 | "editor.formatOnSave": true 16 | }, 17 | "[javascriptreact]": { 18 | "editor.formatOnSave": true 19 | }, 20 | "[json]": { 21 | "editor.formatOnSave": true 22 | }, 23 | "[typescript]": { 24 | "editor.formatOnSave": true 25 | }, 26 | "[typescriptreact]": { 27 | "editor.formatOnSave": true 28 | }, 29 | "editor.lineNumbers": "on", 30 | "editor.rulers": [ 31 | 80 32 | ], 33 | "editor.wordWrap": "on", 34 | "eslint.packageManager": "yarn", 35 | "files.insertFinalNewline": true, 36 | "files.trimTrailingWhitespace": true, 37 | "npm.packageManager": "yarn", 38 | "typescript.enablePromptUseWorkspaceTsdk": true, 39 | "cSpell.words": [ 40 | "bordercomp", 41 | "canonicalize", 42 | "canonicalized", 43 | "canonicalizer", 44 | "prizm" 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # prizm 2 | JAPANESE PREFECTURAL GUESSING GAME! 3 | 4 | https://prizm.pw 5 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | app: 4 | image: node:18 5 | tty: true 6 | ports: 7 | - '3000:3000' 8 | - '6006:6006' 9 | volumes: 10 | - .:/app 11 | working_dir: /app 12 | command: bash -c "yarn install && yarn start" 13 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "database": { 3 | "rules": "database.rules.json" 4 | }, 5 | "hosting": { 6 | "public": "build", 7 | "ignore": [ 8 | "firebase.json", 9 | "**/.*", 10 | "**/node_modules/**" 11 | ], 12 | "rewrites": [ 13 | { 14 | "source": "**", 15 | "destination": "/index.html" 16 | } 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hello-world", 3 | "version": "0.1.0", 4 | "homepage": "./", 5 | "private": true, 6 | "dependencies": { 7 | "@emotion/react": "^11.9.3", 8 | "@reduxjs/toolkit": "^1.8.3", 9 | "@testing-library/jest-dom": "^5.16.4", 10 | "@testing-library/react": "^13.3.0", 11 | "@testing-library/user-event": "^13.5.0", 12 | "@types/jest": "^27.5.2", 13 | "@types/node": "^16.11.45", 14 | "@types/react": "^18.0.15", 15 | "@types/react-dom": "^18.0.6", 16 | "firebase": "^9.9.1", 17 | "firebase-admin": "^11.0.0", 18 | "firebase-tools": "^12.0.0", 19 | "lodash": "^4.17.21", 20 | "normalize.css": "^8.0.1", 21 | "react": "^18.2.0", 22 | "react-dom": "^18.2.0", 23 | "react-icons": "^4.4.0", 24 | "react-redux": "^8.0.2", 25 | "react-scripts": "5.0.1", 26 | "react-ztext": "^1.0.3", 27 | "ts-node": "^10.9.1", 28 | "typescript": "^4.7.4", 29 | "typesync": "^0.9.2", 30 | "web-vitals": "^2.1.4" 31 | }, 32 | "scripts": { 33 | "start": "react-scripts start", 34 | "build": "react-scripts build", 35 | "test": "react-scripts test", 36 | "eject": "react-scripts eject", 37 | "fix": "npm run -s format && npm run -s lint:fix", 38 | "format": "prettier --write --loglevel=warn 'src/**/*.{js,jsx,ts,tsx,gql,graphql,json}'", 39 | "lint": "npm run -s lint:style; npm run -s lint:es", 40 | "lint:fix": "npm run -s lint:style:fix && npm run -s lint:es:fix", 41 | "lint:es": "eslint 'src/**/*.{js,jsx,ts,tsx}'", 42 | "lint:es:fix": "eslint --fix 'src/**/*.{js,jsx,ts,tsx}'", 43 | "lint:conflict": "eslint-config-prettier 'src/**/*.{js,jsx,ts,tsx}'", 44 | "lint:style": "stylelint 'src/**/*.{css,less,sass,scss}'", 45 | "lint:style:fix": "stylelint --fix 'src/**/*.{css,less,sass,scss}'", 46 | "preinstall": "typesync || :" 47 | }, 48 | "eslintConfig": { 49 | "extends": [ 50 | "react-app", 51 | "react-app/jest" 52 | ], 53 | "overrides": [ 54 | { 55 | "files": [ 56 | "**/*.stories.*" 57 | ], 58 | "rules": { 59 | "import/no-anonymous-default-export": "off" 60 | } 61 | } 62 | ] 63 | }, 64 | "browserslist": { 65 | "production": [ 66 | ">0.2%", 67 | "not dead", 68 | "not op_mini all" 69 | ], 70 | "development": [ 71 | "last 1 chrome version", 72 | "last 1 firefox version", 73 | "last 1 safari version" 74 | ] 75 | }, 76 | "devDependencies": { 77 | "@types/eslint": "^8.4.5", 78 | "@types/lodash": "^4.14.182", 79 | "@types/prettier": "^2.6.3", 80 | "@types/prop-types": "^15.7.5", 81 | "@types/testing-library__jest-dom": "^5.14.5", 82 | "@types/testing-library__user-event": "^4.2.0", 83 | "@typescript-eslint/eslint-plugin": "^5.30.7", 84 | "@typescript-eslint/parser": "^5.30.7", 85 | "babel-plugin-named-exports-order": "^0.0.2", 86 | "eslint": "^7.32.0 || ^8.2.0", 87 | "eslint-config-airbnb": "^19.0.4", 88 | "eslint-config-prettier": "^8.5.0", 89 | "eslint-plugin-import": "^2.25.3", 90 | "eslint-plugin-jsx-a11y": "^6.6.1", 91 | "eslint-plugin-prefer-arrow": "^1.2.3", 92 | "eslint-plugin-react": "^7.28.0", 93 | "eslint-plugin-react-hooks": "^4.3.0", 94 | "prettier": "^2.7.1", 95 | "prop-types": "^15.8.1", 96 | "stylelint": "^14.9.1", 97 | "stylelint-config-recess-order": "^3.0.0", 98 | "stylelint-config-standard": "^26.0.0", 99 | "stylelint-order": "^5.0.0", 100 | "webpack": "^5.74.0" 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koluriri/prizm/8705fa0d028537f61939fa37b934890aab7acf8e/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koluriri/prizm/8705fa0d028537f61939fa37b934890aab7acf8e/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koluriri/prizm/8705fa0d028537f61939fa37b934890aab7acf8e/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/audio/button.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koluriri/prizm/8705fa0d028537f61939fa37b934890aab7acf8e/public/audio/button.mp3 -------------------------------------------------------------------------------- /public/audio/cancel.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koluriri/prizm/8705fa0d028537f61939fa37b934890aab7acf8e/public/audio/cancel.mp3 -------------------------------------------------------------------------------- /public/audio/collapse.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koluriri/prizm/8705fa0d028537f61939fa37b934890aab7acf8e/public/audio/collapse.mp3 -------------------------------------------------------------------------------- /public/audio/correct.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koluriri/prizm/8705fa0d028537f61939fa37b934890aab7acf8e/public/audio/correct.mp3 -------------------------------------------------------------------------------- /public/audio/end.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koluriri/prizm/8705fa0d028537f61939fa37b934890aab7acf8e/public/audio/end.mp3 -------------------------------------------------------------------------------- /public/audio/hint.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koluriri/prizm/8705fa0d028537f61939fa37b934890aab7acf8e/public/audio/hint.mp3 -------------------------------------------------------------------------------- /public/audio/incorrect.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koluriri/prizm/8705fa0d028537f61939fa37b934890aab7acf8e/public/audio/incorrect.mp3 -------------------------------------------------------------------------------- /public/audio/music.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koluriri/prizm/8705fa0d028537f61939fa37b934890aab7acf8e/public/audio/music.mp3 -------------------------------------------------------------------------------- /public/audio/offline.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koluriri/prizm/8705fa0d028537f61939fa37b934890aab7acf8e/public/audio/offline.mp3 -------------------------------------------------------------------------------- /public/audio/online.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koluriri/prizm/8705fa0d028537f61939fa37b934890aab7acf8e/public/audio/online.mp3 -------------------------------------------------------------------------------- /public/audio/question.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koluriri/prizm/8705fa0d028537f61939fa37b934890aab7acf8e/public/audio/question.mp3 -------------------------------------------------------------------------------- /public/audio/start.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koluriri/prizm/8705fa0d028537f61939fa37b934890aab7acf8e/public/audio/start.mp3 -------------------------------------------------------------------------------- /public/audio/uncollapse.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koluriri/prizm/8705fa0d028537f61939fa37b934890aab7acf8e/public/audio/uncollapse.mp3 -------------------------------------------------------------------------------- /public/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #f2efe2 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koluriri/prizm/8705fa0d028537f61939fa37b934890aab7acf8e/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koluriri/prizm/8705fa0d028537f61939fa37b934890aab7acf8e/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koluriri/prizm/8705fa0d028537f61939fa37b934890aab7acf8e/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | prizm - 都道府県を当てるだけ 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 |
36 | 37 | 38 | -------------------------------------------------------------------------------- /public/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koluriri/prizm/8705fa0d028537f61939fa37b934890aab7acf8e/public/mstile-150x150.png -------------------------------------------------------------------------------- /public/ogp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koluriri/prizm/8705fa0d028537f61939fa37b934890aab7acf8e/public/ogp.png -------------------------------------------------------------------------------- /public/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.14, written by Peter Selinger 2001-2017 9 | 10 | 12 | 36 | 55 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prizm", 3 | "short_name": "prizm", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#f2efe2", 17 | "background_color": "#f2efe2", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | render(); 7 | const linkElement = screen.getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useEffect, useState } from 'react'; 2 | 3 | import { useDispatch, useSelector } from 'react-redux'; 4 | import { RootState } from 'ducks/rootReducer'; 5 | import { gameSlice } from 'ducks/game'; 6 | 7 | import useUserName from 'hooks/use-username'; 8 | import { useToOnline, useToOffline } from 'hooks/use-toonline'; 9 | import useListenGameAndDeleteUser from 'hooks/use-listengameanddeleteuser'; 10 | 11 | import Game from 'modules/game/game'; 12 | import Home from 'modules/home/home'; 13 | import EditUser from 'modules/edituser/edituser'; 14 | import PrizmFooter from 'components/templates/prizmfooter'; 15 | import Privacy from 'components/pages/privacy'; 16 | import './App.css'; 17 | 18 | const App: FC = () => { 19 | const dispatch = useDispatch(); 20 | 21 | const userKey = useSelector((state: RootState) => state.user.key); 22 | 23 | const gameKey = useSelector((state: RootState) => state.game.key); 24 | const gameObj = useSelector((state: RootState) => state.game.entity); 25 | const { unsetGame } = gameSlice.actions; 26 | 27 | const toOnline = useToOnline(); 28 | const toOffline = useToOffline(); 29 | 30 | const [editUserMode, setEditUserMode] = useState(false); 31 | const [privacyMode, setPrivacyMode] = useState(false); 32 | 33 | useUserName(); 34 | 35 | useEffect(() => { 36 | if (userKey === '' && gameKey === '' && !editUserMode && !privacyMode) 37 | toOnline(); 38 | }, [userKey, gameKey, editUserMode, privacyMode, toOnline]); 39 | 40 | const lastMode = useListenGameAndDeleteUser(); 41 | 42 | useEffect(() => { 43 | if (userKey !== '' && editUserMode) toOffline(); 44 | if (userKey !== '' && privacyMode) toOffline(); 45 | // eslint-disable-next-line react-hooks/exhaustive-deps 46 | }, [editUserMode, privacyMode, userKey]); 47 | 48 | return ( 49 | <> 50 | {!editUserMode && 51 | !privacyMode && 52 | (gameKey !== '' && gameObj ? ( 53 | dispatch(unsetGame())} /> 54 | ) : ( 55 | setEditUserMode(true)} lastMode={lastMode} /> 56 | ))} 57 | {editUserMode && !privacyMode && ( 58 | setEditUserMode(false)} /> 59 | )} 60 | {privacyMode && setPrivacyMode(false)} />} 61 | setPrivacyMode(true)} /> 62 | 63 | ); 64 | }; 65 | 66 | export default App; 67 | -------------------------------------------------------------------------------- /src/assets/data/lakes_archive.txt: -------------------------------------------------------------------------------- 1 | 北海道 2 | 阿寒湖 3 | 厚岸湖 4 | 網走湖 5 | 一菱内湖 6 | ウトナイ湖 7 | 得茂別湖 8 | 生花苗沼 9 | 大沼 10 | 大沼 11 | 大湯沼 12 | 大湯沼 13 | オコタンペ湖 14 | 温根沼 15 | オンネトー 16 | オンネ沼 17 | おんねとう 18 | カムイト沼 19 | キモントウ沼 20 | キモン沼 21 | キモンマ沼 22 | 久種湖 23 | 屈斜路湖 24 | 倶多楽湖 25 | クッチャロ湖 26 | ケラムイ湖 27 | 小沼 28 | 駒止湖 29 | コムケ湖 30 | 佐々田沼 31 | サロマ湖 32 | 然別湖 33 | 支笏湖 34 | 東雲湖 35 | シブノツナイ湖 36 | 蘂取沼 37 | 紗那沼 38 | シュンクシタカラ湖 39 | 蓴菜沼 40 | シラルトロ沼 41 | 知床五湖 42 | 知床沼 43 | 神仙沼 44 | 達古武湖 45 | チミケップ湖 46 | 長節湖 47 | 長節湖 48 | チョマトー 49 | 濤沸湖 50 | 東沸湖 51 | 洞爺湖 52 | 塘路湖 53 | トウロ沼 54 | 年萌湖 55 | 豊似湖 56 | 内保沼 57 | ニキショロ湖 58 | 西ビロク湖 59 | 能取湖 60 | ノトロ湖 61 | 馬主来沼 62 | 春採湖 63 | パンケ沼 64 | パンケトー 65 | 東ビロク湖 66 | 火散布沼 67 | 風蓮湖 68 | ペケレット湖 69 | ペンケ沼 70 | ペンケトー 71 | ポロト湖 72 | ポロ沼 73 | ホロカヤントウ 74 | 摩周湖 75 | 宮島沼 76 | モエレ沼 77 | モケウニ沼 78 | 藻琴湖 79 | 藻散布沼 80 | 湧洞沼 81 | 羅臼湖 82 | ラウス沼 83 | 84 | 85 | 86 | 87 | 青森県 88 | 姉沼 89 | 市柳沼 90 | 宇曽利山湖 91 | 小川原湖 92 | 尾駮沼 93 | 十三湖 94 | 十二湖 95 | 鷹架沼 96 | 田光沼 97 | 田面木沼 98 | 十和田湖 99 | 100 | 101 | 102 | 103 | 宮城県 104 | 伊豆沼 105 | 内沼 106 | 御釜 107 | 鳥の海 108 | 長面浦 109 | 長沼 110 | 万石浦 111 | 112 | 113 | 114 | 115 | 秋田県 116 | 十和田湖 117 | 浅内沼 118 | 作沢沼 119 | 田沢湖 120 | 八郎潟調整池 121 | 目潟 122 | 123 | 124 | 125 | 126 | 山形県 127 | 大鳥池 128 | 白竜湖 129 | 130 | 131 | 132 | 133 | 福島県 134 | 秋元湖 135 | 猪苗代湖 136 | 雄国沼 137 | 尾瀬沼 138 | 小野川湖 139 | 男沼 140 | 五色沼 141 | 曽原湖 142 | 仁田沼 143 | 沼沢湖 144 | 半田沼 145 | 桧原湖 146 | 蓋沼 147 | 松川浦 148 | 女沼 149 | 150 | 151 | 152 | 153 | 茨城県 154 | 牛久沼 155 | 霞ヶ浦 156 | 古利根沼 157 | 菅生沼 158 | 千波湖 159 | 神之池 160 | 涸沼 161 | 162 | 163 | 164 | 165 | 栃木県 166 | 鬼怒沼 167 | 五色沼 168 | 中禅寺湖 169 | 湯ノ湖 170 | 西ノ湖 171 | 172 | 173 | 174 | 175 | 群馬県 176 | 大沼 177 | 尾瀬沼 178 | 菅沼 179 | 多々良沼 180 | 榛名湖 181 | 丸沼 182 | 大尻沼 183 | 184 | 185 | 186 | 187 | 埼玉県 188 | 伊佐沼 189 | 黒浜沼 190 | 柴山沼 191 | 白幡沼 192 | 鳥羽井沼 193 | 原市沼 194 | 別所沼 195 | 196 | 197 | 198 | 199 | 千葉県 200 | 印旛沼 201 | 手賀沼 202 | 与田浦 203 | 204 | 205 | 206 | 207 | 東京都 208 | 不忍池 209 | 洗足池 210 | 211 | 212 | 213 | 214 | 神奈川県 215 | 芦ノ湖 216 | 震生湖 217 | 218 | 219 | 220 | 221 | 新潟県 222 | 加茂湖 223 | 佐潟 224 | 鳥屋野潟 225 | 福島潟 226 | 松浜の池 227 | 228 | 229 | 230 | 231 | 富山県 232 | 十二町潟 233 | ミクリガ池 234 | 縄ヶ池 235 | 刈込池 236 | 多枝原池 237 | 泥鰌池 238 | 釜池 239 | 240 | 241 | 242 | 243 | 石川県 244 | 木場潟 245 | 柴山潟 246 | 河北潟 247 | 邑知潟 248 | 北潟湖 249 | 250 | 251 | 252 | 253 | 福井県 254 | 北潟湖 255 | 水月湖 256 | 菅湖 257 | 久々子湖 258 | 日向湖 259 | 三方湖 260 | 261 | 262 | 263 | 264 | 長野県 265 | 大沼池 266 | 風吹大池 267 | 白駒の池 268 | 諏訪湖 269 | 大正池 270 | 田代池 271 | 青木湖 272 | 木崎湖 273 | 中綱湖 274 | 野尻湖 275 | 白馬大池 276 | 琵琶池 277 | 北竜湖 278 | 松原湖 279 | 280 | 281 | 282 | 283 | 山梨県 284 | 四尾連湖 285 | 河口湖 286 | 西湖 287 | 精進湖 288 | 本栖湖 289 | 山中湖 290 | 291 | 292 | 293 | 294 | 静岡県 295 | 一碧湖 296 | 佐鳴湖 297 | 八丁池 298 | 浜名湖 299 | 300 | 301 | 302 | 303 | 愛知県 304 | 油ヶ淵 305 | 306 | 307 | 308 | 309 | 京都府 310 | 阿蘇海 311 | 久美浜湾 312 | 離湖 313 | 尺八池 314 | 玄覇池 315 | 深泥池 316 | 317 | 318 | 319 | 320 | 滋賀県 321 | 西の湖 322 | 琵琶湖 323 | 余呉湖 324 | 325 | 326 | 327 | 328 | 鳥取県 329 | 中海 330 | 湖山池 331 | 多鯰ヶ池 332 | 東郷池 333 | 水尻池 334 | 335 | 336 | 337 | 338 | 島根県 339 | 神西湖 340 | 宍道湖 341 | 中海 342 | 343 | 344 | 345 | 346 | 徳島県 347 | 海老ヶ池 348 | 349 | 350 | 351 | 352 | 熊本県 353 | 上江津湖 354 | 下江津湖 355 | 356 | 357 | 358 | 359 | 大分県 360 | 志高湖 361 | 362 | 363 | 364 | 365 | 宮崎県 366 | 大幡池 367 | 不動池 368 | 御池 369 | 六観音御池 370 | 371 | 372 | 373 | 374 | 鹿児島県 375 | 池田湖 376 | 藺牟田池 377 | 鰻池 378 | 大浪池 379 | 薩摩湖 380 | 住吉池 381 | なまこ池 382 | 383 | 384 | 385 | 386 | 沖縄県 387 | 大池 388 | -------------------------------------------------------------------------------- /src/assets/data/prefecture.tsx: -------------------------------------------------------------------------------- 1 | export const prefecture = [ 2 | '北海道', 3 | '青森県', 4 | '岩手県', 5 | '宮城県', 6 | '秋田県', 7 | '山形県', 8 | '福島県', 9 | '茨城県', 10 | '栃木県', 11 | '群馬県', 12 | '埼玉県', 13 | '千葉県', 14 | '東京都', 15 | '神奈川県', 16 | '新潟県', 17 | '富山県', 18 | '石川県', 19 | '福井県', 20 | '山梨県', 21 | '長野県', 22 | '岐阜県', 23 | '静岡県', 24 | '愛知県', 25 | '三重県', 26 | '滋賀県', 27 | '京都府', 28 | '大阪府', 29 | '兵庫県', 30 | '奈良県', 31 | '和歌山県', 32 | '鳥取県', 33 | '島根県', 34 | '岡山県', 35 | '広島県', 36 | '山口県', 37 | '徳島県', 38 | '香川県', 39 | '愛媛県', 40 | '高知県', 41 | '福岡県', 42 | '佐賀県', 43 | '長崎県', 44 | '熊本県', 45 | '大分県', 46 | '宮崎県', 47 | '鹿児島県', 48 | '沖縄県', 49 | ] as const; 50 | 51 | export const prefectureABC = { 52 | 北海道: 'HOKKAIDO', 53 | 青森県: 'AOMORI', 54 | 岩手県: 'IWATE', 55 | 宮城県: 'MIYAGI', 56 | 秋田県: 'AKITA', 57 | 山形県: 'YAMAGATA', 58 | 福島県: 'FUKUSHIMA', 59 | 茨城県: 'IBARAKI', 60 | 栃木県: 'TOCHIGI', 61 | 群馬県: 'GUNMA', 62 | 埼玉県: 'SAITAMA', 63 | 千葉県: 'CHIBA', 64 | 東京都: 'TOKYO', 65 | 神奈川県: 'KANAGAWA', 66 | 新潟県: 'NIIGATA', 67 | 富山県: 'TOYAMA', 68 | 石川県: 'ISHIKAWA', 69 | 福井県: 'FUKUI', 70 | 山梨県: 'YAMANASHI', 71 | 長野県: 'NAGANO', 72 | 岐阜県: 'GIFU', 73 | 静岡県: 'SHIZUOKA', 74 | 愛知県: 'AICHI', 75 | 三重県: 'MIE', 76 | 滋賀県: 'SHIGA', 77 | 京都府: 'KYOUTO', 78 | 大阪府: 'OSAKA', 79 | 兵庫県: 'HYOUGO', 80 | 奈良県: 'NARA', 81 | 和歌山県: 'WAKAYAMA', 82 | 鳥取県: 'TOTTORI', 83 | 島根県: 'SHIMANE', 84 | 岡山県: 'OKAYAMA', 85 | 広島県: 'HIROSHIMA', 86 | 山口県: 'YAMAGUCHI', 87 | 徳島県: 'TOKUSHIMA', 88 | 香川県: 'KAGAWA', 89 | 愛媛県: 'EHIME', 90 | 高知県: 'KOUCHI', 91 | 福岡県: 'FUKUOKA', 92 | 佐賀県: 'SAGA', 93 | 長崎県: 'NAGASAKI', 94 | 熊本県: 'KUMAMOTO', 95 | 大分県: 'OOITA', 96 | 宮崎県: 'MIYAZAKI', 97 | 鹿児島県: 'KAGOSHIMA', 98 | 沖縄県: 'OKINAWA', 99 | }; 100 | 101 | export const hint: { 102 | [pref: string]: [number, number, string, 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8]; 103 | } = { 104 | 北海道: [538, 83424, 's', 1], 105 | 宮城県: [233, 7282, 's', 1], 106 | 福島県: [191, 13784, 'f', 1], 107 | 青森県: [131, 9646, 'a', 1], 108 | 岩手県: [128, 15275, 'm', 1], 109 | 山形県: [112, 9323, 'y', 1], 110 | 秋田県: [102, 11638, 'a', 1], 111 | 東京都: [1352, 2191, 's', 2], 112 | 神奈川県: [913, 2416, 'y', 2], 113 | 埼玉県: [727, 3798, 's', 2], 114 | 千葉県: [622, 5158, 'c', 2], 115 | 茨城県: [292, 6097, 'm', 2], 116 | 群馬県: [197, 6362, 'm', 2], 117 | 栃木県: [197, 6408, 'u', 2], 118 | 長野県: [210, 13562, 'n', 2], 119 | 山梨県: [83, 4465, 'k', 2], 120 | 新潟県: [230, 12584, 'n', 2], 121 | 愛知県: [748, 5172, 'n', 3], 122 | 岐阜県: [203, 10621, 'g', 3], 123 | 静岡県: [370, 7777, 's', 3], 124 | 三重県: [182, 5774, 't', 3], 125 | 石川県: [115, 4186, 'k', 4], 126 | 富山県: [107, 4248, 't', 4], 127 | 福井県: [79, 4190, 'f', 4], 128 | 大阪府: [884, 1905, 'o', 5], 129 | 兵庫県: [553, 8401, 'k', 5], 130 | 京都府: [261, 4612, 'k', 5], 131 | 滋賀県: [141, 4017, 'o', 5], 132 | 奈良県: [136, 3691, 'n', 5], 133 | 和歌山県: [96, 4725, 'w', 5], 134 | 広島県: [284, 8479, 'h', 6], 135 | 岡山県: [192, 7115, 'o', 6], 136 | 山口県: [140, 6112, 'y', 6], 137 | 鳥取県: [57, 3507, 't', 6], 138 | 島根県: [69, 6708, 'm', 6], 139 | 香川県: [98, 187, 't', 7], 140 | 愛媛県: [139, 5676, 'm', 7], 141 | 徳島県: [76, 4147, 't', 7], 142 | 高知県: [73, 7104, 'k', 7], 143 | 福岡県: [510, 4986, 'f', 8], 144 | 鹿児島県: [165, 9187, 'k', 8], 145 | 熊本県: [179, 7409, 'k', 8], 146 | 長崎県: [138, 4132, 'n', 8], 147 | 宮崎県: [110, 7735, 'm', 8], 148 | 大分県: [117, 6341, 'o', 8], 149 | 佐賀県: [83, 2441, 's', 8], 150 | 沖縄県: [143, 2281, 'n', 8], 151 | }; 152 | 153 | export const area = { 154 | 1: '東北または北海道', 155 | 2: '関東甲信越地方', 156 | 3: '東海地方', 157 | 4: '北陸地方', 158 | 5: '近畿地方', 159 | 6: '中国地方', 160 | 7: '四国地方', 161 | 8: '九州または沖縄', 162 | }; 163 | -------------------------------------------------------------------------------- /src/assets/data/specialties.tsx: -------------------------------------------------------------------------------- 1 | import { DefinedQuestions } from 'utils/types'; 2 | 3 | const specialties = (): DefinedQuestions => ({ 4 | 北海道: [ 5 | 'じゃがいも', 6 | '小麦', 7 | 'たまねぎ', 8 | 'にんじん', 9 | 'だいこん', 10 | 'スイートコーン', 11 | 'かぼちゃ', 12 | 'ヤマノイモ', 13 | 'ブロッコリー', 14 | 'アスパラガス', 15 | 'かぶ(大野紅かぶ)', 16 | '乳製品', 17 | '鱈', 18 | '鮭', 19 | 'ししゃも', 20 | 'ラワンぶき', 21 | 'ガラナ', 22 | 'さくらんぼ', 23 | 'ハスカップ', 24 | 'アロニア', 25 | 'シーベリー', 26 | 'スグリ', 27 | 'キイチゴ', 28 | '甜菜', 29 | '大豆', 30 | '小豆', 31 | '蕎麦', 32 | '乾燥インゲン', 33 | '菜種', 34 | 'ホタテ', 35 | 'スケトウダラ', 36 | 'サケ', 37 | 'コンブ', 38 | 'マダラ', 39 | 'ホッケ', 40 | 'マイワシ', 41 | 'カレイ', 42 | 'タコ', 43 | 'サンマ', 44 | 'ニシン', 45 | 'スルメイカ', 46 | 'イカナゴ', 47 | 'ウニ', 48 | 'マス', 49 | 'ナマコ', 50 | 'ベニズワイガニ', 51 | 'ヒラメ', 52 | 'ズワイガニ', 53 | 'フグ', 54 | 'キンキ', 55 | ], 56 | 青森県: [ 57 | 'ごぼう', 58 | 'りんご', 59 | 'にんにく', 60 | '食用菊', 61 | 'ひめます', 62 | 'ホタテガイ', 63 | 'するめいか', 64 | 'さば', 65 | 'ほや', 66 | 'めかぶとろろ', 67 | 'ばっけ味噌(ふきのとう味噌)', 68 | 'アンズ', 69 | 'クルミ', 70 | 'フサスグリ', 71 | 'アカイカ', 72 | 'ワカサギ', 73 | 'シラウオ', 74 | 'ウグイ・オイカワ', 75 | 'コイ', 76 | ], 77 | 岩手県: [ 78 | 'りんご', 79 | '干し椎茸', 80 | '二子の里芋', 81 | '養殖わかめ', 82 | '鮭', 83 | 'うに', 84 | 'サメ', 85 | 'さんま', 86 | 'するめいか', 87 | '鶏肉', 88 | 'めかぶとろろ', 89 | 'トシロ', 90 | 'ヤマブドウ', 91 | 'オキアミ', 92 | 'アワビ', 93 | ], 94 | 宮城県: [ 95 | '茗荷茸', 96 | 'セリ', 97 | 'サメ', 98 | 'かじき', 99 | '養殖ギンザケ', 100 | 'わかめ', 101 | '養殖牡蛎', 102 | 'パプリカ', 103 | 'メバチマグロ', 104 | 'メカジキ', 105 | '仙台いちご', 106 | '仙台牛', 107 | '仙台黒毛和牛', 108 | '仙台味噌', 109 | '長なす', 110 | ], 111 | 秋田県: [ 112 | 'とんぶり', 113 | 'ジュンサイ', 114 | '米', 115 | 'りんご', 116 | '白神みょうが', 117 | '白神長ネギ', 118 | '秋田フキ', 119 | '比内地鶏', 120 | 'とんぶり', 121 | 'ハタハタ', 122 | 'ギバサ(アカモク)', 123 | 'サラダ寒天', 124 | 'うど', 125 | '花みょうが', 126 | 'ラズベリー', 127 | 'ブラックベリー', 128 | 'ワカサギ', 129 | ], 130 | 山形県: [ 131 | '桜桃', 132 | 'ラ・フランス', 133 | 'たらの芽', 134 | 'なめこ', 135 | 'ブドウ', 136 | '平田ねぎ', 137 | 'じんだん', 138 | 'もってのほか(食用菊)', 139 | 'すいか', 140 | '枝豆', 141 | 'マッシュルーム', 142 | '芽キャベツ', 143 | '西洋なし', 144 | 'さくらんぼ', 145 | 'アケビ', 146 | 'カリン', 147 | ], 148 | 福島県: [ 149 | 'つるむらさき', 150 | '桃', 151 | 'さやいんげん', 152 | 'きゅうり', 153 | 'タコ', 154 | 'さんま', 155 | 'あんぽ柿', 156 | '馬肉', 157 | 'さやえんどう', 158 | 'サルナシ', 159 | 'ナツハゼ', 160 | 'マイワシ', 161 | ], 162 | 茨城県: [ 163 | '干し芋', 164 | 'エシャロット', 165 | 'レンコン', 166 | '栗', 167 | 'メロン', 168 | 'さつまいも', 169 | 'ゴボウ', 170 | '米', 171 | 'ハマグリ', 172 | 'ワカサギ', 173 | 'ピーマン', 174 | '水菜', 175 | '小松菜', 176 | 'チンゲンサイ', 177 | '日本なし', 178 | '栗', 179 | 'マイワシ', 180 | 'エビ', 181 | 'うなぎ', 182 | 'ハゼ', 183 | 'コイ', 184 | ], 185 | 栃木県: [ 186 | 'かんぴょう', 187 | '二条大麦', 188 | 'いちご', 189 | 'クレソン', 190 | 'ウド', 191 | 'ニラ', 192 | '唐辛子', 193 | '中山かぼちゃ', 194 | '氏家うどん', 195 | '真岡木綿', 196 | ], 197 | 群馬県: [ 198 | 'こんにゃく', 199 | '花ニラ', 200 | 'ウド', 201 | 'きゅうり', 202 | 'キャベツ(嬬恋高原)', 203 | '椎茸', 204 | 'ねぎ(下仁田ネギ)', 205 | '国分ニンジン', 206 | '陣田みょうが', 207 | '花いんげん', 208 | 'キャベツ', 209 | '枝豆', 210 | 'モロヘイヤ', 211 | ], 212 | 埼玉県: [ 213 | 'クワイ', 214 | '小松菜', 215 | 'ネギ', 216 | 'ほうれん草', 217 | 'カブ', 218 | '川越さつまいも', 219 | '越谷ネギ', 220 | '里芋', 221 | '北本トマトカレー', 222 | '草加せんべい', 223 | '河越抹茶', 224 | ], 225 | 千葉県: [ 226 | '落花生', 227 | 'カブ', 228 | '里芋', 229 | 'ねぎ', 230 | 'さやいんげん', 231 | 'みつば', 232 | 'マッシュルーム', 233 | 'カリン', 234 | 'コノシロ', 235 | 'スズキ', 236 | '梨', 237 | ], 238 | 東京都: [ 239 | '小松菜', 240 | '水わさび', 241 | 'ウド', 242 | '稲城の梨', 243 | 'キャベツ', 244 | 'カリフラワー', 245 | 'カジキ', 246 | '鰹', 247 | 'トビウオ', 248 | 'パッションフルーツ', 249 | 'レモン', 250 | ], 251 | 神奈川県: [ 252 | '大根', 253 | 'キャベツ', 254 | 'ほうれん草', 255 | '温州みかん', 256 | 'キウイ', 257 | 'マグロ', 258 | 'ひじき菜', 259 | 'おかのり', 260 | '冬瓜', 261 | 'モロヘイヤ', 262 | 'エシャレット', 263 | '芽キャベツ', 264 | '湘南ゴールド', 265 | 'アカイカ', 266 | 'アユ', 267 | ], 268 | 新潟県: [ 269 | '米', 270 | 'マイタケ', 271 | 'エリンギ', 272 | '大根', 273 | '漬物用ナス', 274 | 'おけさ柿', 275 | '切り餅', 276 | '包装餅', 277 | 'かんずり', 278 | '西洋なし', 279 | 'アロニア', 280 | 'ベニズワイガニ', 281 | 'フナ', 282 | 'コイ', 283 | 'サクラマス', 284 | ], 285 | 富山県: [ 286 | '黒部米', 287 | '里芋', 288 | '五箇山かぼちゃ', 289 | 'ぶり', 290 | 'さんま', 291 | 'かに', 292 | '甘えび', 293 | '白えび', 294 | 'ホタルえび', 295 | 'ゲンゲ', 296 | '六条大麦', 297 | 'ソウダガツオ', 298 | 'サクラマス', 299 | ], 300 | 石川県: [ 301 | '枯露柿', 302 | 'てんば菜', 303 | 'ぶり', 304 | 'イワシ', 305 | 'サバ', 306 | '甘えび', 307 | 'ニギス', 308 | 'フグ', 309 | 'サワラ', 310 | 'スルメイカ', 311 | '中島菜', 312 | '加賀野菜', 313 | '能登大納言', 314 | '能登米', 315 | '能登棚田米', 316 | '能登牛', 317 | '能登ふぐ', 318 | '加賀棒茶', 319 | ], 320 | 福井県: [ 321 | 'ラッキョウ(花ラッキョウ)', 322 | '梅', 323 | '越前がに', 324 | 'サバ', 325 | 'アマダイ(若狭ぐじ)', 326 | '若狭がれい', 327 | '若狭ふぐ', 328 | '越前うに', 329 | '甘えび', 330 | 'たらの子のかんづめ', 331 | '粉わかめ', 332 | 'オーロラ印の味付たら', 333 | 'ナツメ', 334 | '六条大麦', 335 | 'サワラ', 336 | ], 337 | 山梨県: [ 338 | 'クレソン', 339 | '桃', 340 | 'すもも', 341 | 'ぶどう', 342 | '芽キャベツ', 343 | '桜桃', 344 | '甲州小梅', 345 | 'スイートコーン', 346 | 'なす', 347 | 'きゅうり', 348 | 'トマト', 349 | '漬物', 350 | '八幡芋', 351 | 'さくらんぼ', 352 | 'ネクタリン', 353 | 'ブラックベリー', 354 | 'カリン', 355 | ], 356 | 長野県: [ 357 | 'えのきたけ', 358 | 'セロリ', 359 | 'レタス', 360 | 'リンゴ', 361 | '干し柿', 362 | '寒天', 363 | '白菜', 364 | 'レタス', 365 | 'サニーレタス', 366 | 'ズッキーニ', 367 | 'パセリ', 368 | 'りんご', 369 | 'ぶどう', 370 | 'プルーン', 371 | 'ネクタリン', 372 | 'ブルーベリー', 373 | 'マルメロ', 374 | 'くるみ', 375 | '蕎麦', 376 | ], 377 | 岐阜県: [ 378 | '赤カブ', 379 | 'カイワレダイコン', 380 | '柿', 381 | '栗', 382 | 'こんにゃく', 383 | '山菜', 384 | 'トマト', 385 | 'みかん', 386 | '美人姫(いちご)', 387 | 'みょうが', 388 | '鮎', 389 | ], 390 | 静岡県: [ 391 | '芽キャベツ', 392 | 'わさび', 393 | '茶', 394 | '三ヶ日みかん', 395 | 'ネギ', 396 | '玉ねぎ', 397 | '大根', 398 | 'ほうれん草', 399 | '石垣イチゴ', 400 | 'カツオ', 401 | 'キハダマグロ', 402 | 'マグロ', 403 | '桜エビ', 404 | '興津鯛', 405 | 'ゲホウ', 406 | 'ルッコラ', 407 | 'ポポー', 408 | 'ネーブルオレンジ', 409 | 'ダイダイ', 410 | '黄金柑', 411 | 'グレープフルーツ', 412 | 'はるひ', 413 | 'レモネード', 414 | 'オレンジ日向', 415 | 'マーコット', 416 | ], 417 | 愛知県: [ 418 | '食用菊', 419 | 'フキ', 420 | 'シソ', 421 | 'キャベツ', 422 | 'ブロッコリー', 423 | '鶏卵', 424 | 'ウズラ', 425 | 'アサリ', 426 | 'しらす', 427 | 'いかなご', 428 | 'かれい', 429 | 'ウナギ', 430 | '海苔', 431 | 'ラディシュ', 432 | '銀杏', 433 | '夕焼け姫', 434 | 'シラス', 435 | 'アサリ', 436 | 'ワタリガニ', 437 | 'クロダイ', 438 | 'クルマエビ', 439 | ], 440 | 三重県: [ 441 | '茶', 442 | 'カツオ', 443 | 'サバ', 444 | 'カタクチイワシ', 445 | 'ビンチョウマグロ', 446 | 'イセエビ', 447 | 'サザエ', 448 | '養殖マダイ', 449 | '伊勢ヒジキ', 450 | '梅', 451 | 'サマーフレッシュ', 452 | 'セミノール', 453 | 'カラ', 454 | '新姫', 455 | 'ジャバラ', 456 | ], 457 | 滋賀県: [ 458 | '水口かんぴょう', 459 | 'ボイセンベリー', 460 | 'カブ', 461 | '六条大麦', 462 | '小麦', 463 | '草津メロン', 464 | '子小鮎', 465 | '赤こんにゃく', 466 | 'モリヤマメロン', 467 | '近江牛', 468 | '琵琶湖産鮎', 469 | '甲賀のお茶', 470 | '政所茶', 471 | ], 472 | 京都府: [ 473 | '京野菜', 474 | 'みずな', 475 | 'くわい', 476 | '京みず菜', 477 | '賀茂なす', 478 | '伏見とうがらし', 479 | 'えびいも', 480 | '九条ねぎ', 481 | '京たけのこ', 482 | '鹿ケ谷かぼちゃ', 483 | '堀川ごぼう', 484 | '聖護院だいこん', 485 | '京壬生菜(みぶな)', 486 | '京山科なす', 487 | '聖護院かぶ', 488 | '万願寺甘とう', 489 | '花菜', 490 | ], 491 | 大阪府: [ 492 | 'しゅんぎく', 493 | 'ふき', 494 | 'かいわれだいこん', 495 | 'タデ', 496 | 'イチジク', 497 | 'シラス', 498 | 'コノシロ', 499 | '泉州水なす', 500 | '八尾若ごぼう', 501 | '泉だこ', 502 | ], 503 | 兵庫県: [ 504 | '玉ねぎ', 505 | 'レタス', 506 | 'いかなご', 507 | '養殖海苔', 508 | 'タコ', 509 | '明石鯛', 510 | '山椒', 511 | '山桃', 512 | '小豆', 513 | ], 514 | 奈良県: [ 515 | '柿', 516 | '梅', 517 | 'エンドウ', 518 | '吉野葛', 519 | '祝ダイコン', 520 | '大和肉鶏', 521 | '吉野桜鮎', 522 | '花みょうが', 523 | '結崎ネブカ', 524 | '大和肉鶏', 525 | '吉野本葛', 526 | '吉野葛', 527 | '平群の小菊', 528 | ], 529 | 和歌山県: [ 530 | '下津みかん', 531 | '有田みかん', 532 | '紀州南高梅', 533 | '紀ノ川柿', 534 | 'じゃばら', 535 | 'うすいえんどう', 536 | 'グリーンピース', 537 | 'イチジク', 538 | '山椒', 539 | 'アボカド', 540 | '八朔', 541 | '清見', 542 | 'セミノール', 543 | '三宝柑', 544 | 'バレンシアオレンジ', 545 | 'ジャバラ', 546 | '津之望', 547 | '春峰', 548 | ], 549 | 鳥取県: [ 550 | '砂丘らっきょう', 551 | '花御所柿', 552 | '二十世紀梨', 553 | 'すいか', 554 | 'かに', 555 | 'ブリ', 556 | 'アジ', 557 | '出雲わかめ', 558 | '十六島のり', 559 | 'ハタハタ', 560 | '大山ブロッコリー', 561 | '日南トマト', 562 | '東伯和牛', 563 | '東伯牛', 564 | ], 565 | 島根県: [ 566 | '津田カブ', 567 | '雲州人参', 568 | '秋鹿ごぼう', 569 | 'しじみ', 570 | 'アジ', 571 | 'ブリ', 572 | 'かに', 573 | '十六島海苔', 574 | 'ウルメイワシ', 575 | 'うなぎ', 576 | '多伎いちじく', 577 | '石見和牛肉', 578 | '隠岐牛', 579 | 'しまね和牛', 580 | '奥出雲和牛', 581 | ], 582 | 岡山県: [ 583 | 'マッシュルーム', 584 | 'トウガン', 585 | '備前ダイコン', 586 | 'そうめん南瓜', 587 | 'ブドウ(マスカット)', 588 | '柿', 589 | 'シャコ', 590 | '牡蠣', 591 | 'フナ', 592 | '藤田レタス', 593 | '牧石ねぎ', 594 | '明治ごんぼう', 595 | '岡山白桃', 596 | '千屋牛', 597 | ], 598 | 広島県: [ 599 | 'ワケギ', 600 | 'クワイ', 601 | 'マツタケ', 602 | 'ネーブルオレンジ', 603 | '八朔', 604 | '温州みかん', 605 | '柿', 606 | 'タデ', 607 | 'オリーブ', 608 | 'レモン', 609 | 'ハルカ', 610 | '安政柑', 611 | '西之香', 612 | '牡蠣', 613 | '福山のくわい', 614 | '三次ピオーネ', 615 | '大長みかん', 616 | '大長レモン', 617 | '比婆牛', 618 | ], 619 | 山口県: [ 620 | '安平麩', 621 | 'レンコン', 622 | '厚保栗', 623 | '養殖鯛', 624 | '養殖エビ', 625 | 'アジ', 626 | 'うに', 627 | 'ふぐ', 628 | 'おばいけ', 629 | 'セトミ', 630 | '酢橙', 631 | '長門ユズキチ', 632 | 'アマダイ', 633 | '厚保くり', 634 | '長州黒かしわ', 635 | '長州地どり', 636 | '下関ふく', 637 | '下関うに', 638 | '北浦うに', 639 | ], 640 | 徳島県: [ 641 | '白瓜', 642 | 'カリフラワー', 643 | '大根', 644 | '鳴門金時', 645 | 'レンコン', 646 | '鳴門ワカメ', 647 | 'にんじん', 648 | '山桃', 649 | 'スダチ', 650 | 'ユコウ', 651 | '柚子', 652 | '渭東ねぎ', 653 | 'なると金時', 654 | '鳴門らっきょ', 655 | '阿波山田錦', 656 | ], 657 | 香川県: [ 658 | 'オリーブ', 659 | 'ニンニク', 660 | 'レタス', 661 | 'サルナシ', 662 | '西南のひかり', 663 | 'カンキツ中間母本農6号', 664 | 'はだか麦', 665 | 'イカナゴ', 666 | '讃岐牛', 667 | 'ひけた鰤', 668 | '伊吹いりこ', 669 | '小豆島オリーブオイル', 670 | ], 671 | 愛媛県: [ 672 | '温州みかん', 673 | 'イヨカン', 674 | 'キウイフルーツ', 675 | '里芋', 676 | '養殖鯛', 677 | '養殖ブリ', 678 | '栗', 679 | 'アボカド', 680 | 'くるみ', 681 | '伊予柑', 682 | '河内晩柑', 683 | 'ポンカン', 684 | 'せとか', 685 | '紅まどんな', 686 | '甘平', 687 | 'カラ', 688 | 'ハルミ', 689 | 'ハレヒメ', 690 | '南津海', 691 | 'ハルカ', 692 | 'ブラッドオレンジ', 693 | '天草', 694 | 'ライム', 695 | '弓削瓢柑', 696 | 'ヒメノツキ', 697 | 'マリヒメ', 698 | 'はだか麦', 699 | 'タチウオ', 700 | '養殖マダイ', 701 | '養殖シマアジ', 702 | '宇和島じゃこ天', 703 | ], 704 | 高知県: [ 705 | '生姜', 706 | '黄金生姜', 707 | 'ニラ', 708 | 'なす', 709 | 'ピーマン', 710 | 'トマト', 711 | '青海苔', 712 | '鰹', 713 | 'マグロ', 714 | 'シイラ', 715 | 'ドロメ(シラス)', 716 | '花みょうが', 717 | '柚子', 718 | '文旦', 719 | '餅柚', 720 | 'ビンチョウマグロ', 721 | 'ソウダガツオ', 722 | 'クロカジキ', 723 | 'ヘダイ', 724 | '徳谷トマト', 725 | '土佐あかうし', 726 | '四万十川の青のり', 727 | '四万十川の青さのり', 728 | ], 729 | 福岡県: [ 730 | 'かいわれだいこん', 731 | '博多なす', 732 | '合馬たけのこ', 733 | 'キウイフルーツ', 734 | 'イチゴ', 735 | 'アサリ', 736 | 'ムツゴロウ', 737 | 'ウナギ', 738 | '海苔', 739 | 'タデ', 740 | 'パクチー', 741 | '果のしずく', 742 | '木酢', 743 | '合馬たけのこ', 744 | '博多蕾菜', 745 | 'はかた地どり', 746 | '鐘崎天然とらふく', 747 | '糸島カキ', 748 | '鐘崎天然とらふく', 749 | '八女茶', 750 | '八女抹茶', 751 | ], 752 | 佐賀県: [ 753 | '二条大麦', 754 | '小麦', 755 | '玉ねぎ', 756 | 'アスパラガス', 757 | 'レンコン', 758 | '佐賀海苔', 759 | 'あげまき', 760 | '麗紅', 761 | '津之輝', 762 | '女山大根', 763 | '伊万里梨', 764 | 'うれしの茶', 765 | ], 766 | 長崎県: [ 767 | '豆乳', 768 | 'びわ', 769 | '白菜', 770 | 'なす', 771 | 'じゃがいも', 772 | 'アジ', 773 | '鯛', 774 | 'ブリ', 775 | '五島するめ', 776 | '紅まどか', 777 | 'ユウコウ', 778 | 'サバ', 779 | 'マアジ', 780 | 'カタクチイワシ', 781 | 'ぶり', 782 | 'マダイ', 783 | 'キダイ', 784 | 'クロマグロ', 785 | 'イサキ', 786 | 'サザエ', 787 | 'チダイ', 788 | '養殖フグ', 789 | '五島牛', 790 | '壱岐牛', 791 | '九十九島かき', 792 | '小長井牡蠣', 793 | ], 794 | 熊本県: [ 795 | 'トマト', 796 | 'すいか', 797 | '夏みかん', 798 | '熊本赤ナス', 799 | '阿蘇高菜', 800 | 'シンデレラ太秋柿', 801 | '天草黒牛', 802 | '天草ぶり', 803 | '養殖マダイ', 804 | '馬肉', 805 | 'カリフラワー', 806 | 'スナップえんどう', 807 | '不知火(デコポン)', 808 | '肥の豊', 809 | '晩白柚', 810 | '大橘', 811 | 'スイートスプリング', 812 | 'ミハヤ', 813 | 'マーコット', 814 | '荒尾梨', 815 | ], 816 | 大分県: [ 817 | '干し椎茸', 818 | 'かぼちゃ', 819 | 'ミツバ', 820 | 'セリ', 821 | '唐辛子', 822 | 'ポンカン', 823 | '養殖ブリ', 824 | '関アジ', 825 | '関サバ', 826 | 'カボス', 827 | 'ジャボン', 828 | '養殖ヒラメ', 829 | '玖珠米', 830 | '日田梨', 831 | '豊後牛', 832 | '岬ガザミ', 833 | ], 834 | 宮崎県: [ 835 | 'マンゴー', 836 | 'ピーマン', 837 | '里芋', 838 | 'きゅうり', 839 | '鶏肉', 840 | '北浦灘アジ', 841 | '門川スズキ', 842 | 'きゅうり', 843 | 'ライチ', 844 | '日向夏', 845 | '金柑', 846 | 'ヘイベイズ', 847 | '早香', 848 | 'ノバ', 849 | '南風', 850 | '南香', 851 | 'ムロアジ', 852 | 'マカジキ', 853 | '都城和牛', 854 | '高千穂牛', 855 | ], 856 | 鹿児島県: [ 857 | 'さつまいも', 858 | 'オクラ', 859 | 'ソラマメ', 860 | 'デコポン', 861 | '養殖ブリ', 862 | '養殖ウナギ', 863 | '養殖クルマエビ', 864 | '養殖カンパチ', 865 | 'さやえんどう', 866 | 'オクラ', 867 | 'スナップえんどう', 868 | 'パッションフルーツ', 869 | 'パパイア', 870 | 'ナツミカン', 871 | 'タンカン', 872 | 'キシュウミカン', 873 | '黒島ミカン', 874 | 'ミナミマグロ', 875 | '知覧紅', 876 | '桜島小みかん', 877 | '赤鶏さつま', 878 | '枕崎鰹節', 879 | '指宿鰹節', 880 | '知覧茶', 881 | '霧島茶', 882 | ], 883 | 沖縄県: [ 884 | 'コンビーフハッシュ', 885 | 'パイナップル', 886 | 'マンゴー', 887 | 'バナナ', 888 | 'トウガン', 889 | 'ドラゴンフルーツ', 890 | 'アセロラ', 891 | 'アテモヤ', 892 | 'グアバ', 893 | 'ゴーヤ', 894 | '黒糖', 895 | '足テビチ', 896 | 'シークヮーサー', 897 | 'カーブチー', 898 | '大紅みかん', 899 | 'サトウキビ', 900 | '養殖もずく', 901 | '養殖クルマエビ', 902 | '石垣牛', 903 | '八重山かまぼこ', 904 | '海ぶどう', 905 | ], 906 | }); 907 | 908 | export default specialties; 909 | -------------------------------------------------------------------------------- /src/assets/data/sweets.tsx: -------------------------------------------------------------------------------- 1 | import { DefinedQuestions } from 'utils/types'; 2 | 3 | const sweets = (): DefinedQuestions => ({ 4 | 北海道: [ 5 | '白い恋人', 6 | '札幌タイムズスクエア', 7 | '中花まんじゅう', 8 | 'ロイズ生チョコレート', 9 | '月寒あんぱん', 10 | 'みそまんじゅう', 11 | 'トラピストクッキー', 12 | 'わかさいも', 13 | 'よいとまけ', 14 | '旭豆', 15 | '氷点下41度', 16 | '花畑牧場生キャラメル', 17 | '六花亭マルセイバターサンド', 18 | 'ねこのたまご', 19 | 'オランダせんべい', 20 | 'バタークッキー', 21 | 'いかいかクッキー', 22 | '阿寒シンプイ', 23 | 'まりも羊羹', 24 | '六花のつゆ', 25 | '六花亭ストロベリーチョコ', 26 | '美冬', 27 | '丹頂鶴の卵', 28 | 'シシャモ使ってないのに、ししゃもパイ', 29 | ], 30 | 31 | 青森県: [ 32 | '気になるリンゴ', 33 | 'パティシエのりんごスティック', 34 | 'バナナ最中', 35 | 'くぢら餅', 36 | '薄紅 たわわ', 37 | '雪逍遥', 38 | 'やっこいサブレ', 39 | '旅さち', 40 | '茶屋の餅', 41 | ], 42 | 43 | 岩手県: [ 44 | 'かもめのたまご', 45 | 'くるみゆべし', 46 | '小岩井バタークッキー', 47 | '生南部サブレ', 48 | 'チロルのクリームチーズケーキ', 49 | '南部せんべい', 50 | 'ごま摺り団子', 51 | '不来方バウム', 52 | '饗の山', 53 | 'もりおか絵巻', 54 | '岩泉ヨーグルト', 55 | 'がんづき', 56 | '岩谷堂羊羹', 57 | '南部煎餅ピーナッツ', 58 | '田むらの梅', 59 | ], 60 | 61 | 宮城県: [ 62 | '喜久福生クリーム大福', 63 | '萩の月', 64 | '支倉焼', 65 | '白松がモナカ', 66 | '仙台駄菓子', 67 | '生クリーム大福 雪ふたえ', 68 | 'かっぱまんじゅう 吉田屋', 69 | '喜久福', 70 | '黒砂糖まんじゅう', 71 | 'モナカ', 72 | '三色最中', 73 | '九重', 74 | ], 75 | 76 | 秋田県: [ 77 | '秋田諸越', 78 | 'いぶりがっこ', 79 | '金のバターもち', 80 | '稲庭饂飩', 81 | 'だまこ餅', 82 | 'しとぎ豆がき', 83 | '西明寺栗', 84 | '金萬', 85 | 'さなづら', 86 | '丹尺もろこし', 87 | '明けがらす', 88 | 'ミニ練屋バナナ', 89 | '練屋バナナ', 90 | '秋田犬もろこし', 91 | ], 92 | 93 | 山形県: [ 94 | '酒田むすめ', 95 | '木村屋 古鏡', 96 | '杵屋本店 山形旬香菓', 97 | '元祖じんだん饅頭', 98 | 'ぱんどら 最上川あわゆき', 99 | '山形まるごとサンド', 100 | 'だだちゃ豆せんべい', 101 | 'からから煎餅', 102 | '蔵王高原チーズケーキ', 103 | 'でん六 蔵王の森', 104 | '樹氷ロマンパートII', 105 | 'のし梅', 106 | 'あじさい紀行', 107 | '古鏡', 108 | 'だだちゃ豆饅頭', 109 | '楽', 110 | ], 111 | 112 | 福島県: [ 113 | '柏屋薄皮饅頭こしあん', 114 | '麦せんべい', 115 | '大黒屋のくるみゆべし', 116 | 'やわらか桃餅 三段', 117 | '家伝ゆべし', 118 | 'あわまんじゅう', 119 | 'あわまんじゅう', 120 | 'カリントまんじゅう', 121 | '会津の天神さま', 122 | 'じゃんがら', 123 | 'ままどおる', 124 | 'エキソンパイ', 125 | '花ことば', 126 | '家伝ゆべし', 127 | '凍天', 128 | 'くまたぱん', 129 | '玉羊羹', 130 | '香木実', 131 | 'ミニだるま最中', 132 | 'いもくり佐太郎', 133 | 'ごえんバウム', 134 | '檸檬(れも)', 135 | ], 136 | 137 | 茨城県: [ 138 | '水戸の梅', 139 | '館最中', 140 | '亀印水戸納豆せんべい', 141 | 'みやびの梅', 142 | '吉原殿中', 143 | '茨城いちごの初恋', 144 | '日立かすていら', 145 | 'ほっしぃ~も', 146 | 'メロンバウム', 147 | 'おみたまプリン', 148 | 'れんこんサブレー ハスだっぺ', 149 | '大みか饅頭', 150 | 'べっ甲ほしいも', 151 | ], 152 | 153 | 栃木県: [ 154 | '苺のたまご', 155 | '日昇堂 葵きんつば', 156 | 'いちごふる里', 157 | '大麦ダクワーズ', 158 | '黄ぶなっこ最中', 159 | '宮のかりまん', 160 | '日光甚五郎煎餅', 161 | 'みかも山', 162 | 'きぬの清流', 163 | 'るかんた', 164 | '日光ぷりん', 165 | 'ニルバーナ', 166 | ], 167 | 168 | 群馬県: [ 169 | 'かりんとうまんじゅう', 170 | '湯の花まんじゅう', 171 | '下仁田ねぎ煎餅', 172 | '旅がらす', 173 | '殿様ねぎ煎餅', 174 | '伊香保美人', 175 | 'キャラメルレーズンサンド', 176 | '群馬ガトーショコラ', 177 | '高原花まめ蒸きんつば', 178 | '湯ったり温泉たまごケーキ', 179 | '北軽井沢レアチーズケーキ', 180 | ], 181 | 182 | 埼玉県: [ 183 | '草加せんべい', 184 | '彩果の宝石', 185 | '十万石まんじゅう', 186 | '白鷺宝', 187 | 'おかしさんのクッキー', 188 | '元祖ねぎみそ煎餅', 189 | '一里飴', 190 | 'いも恋', 191 | '五家宝', 192 | '福蔵', 193 | ], 194 | 195 | 千葉県: [ 196 | '鯛せんべい', 197 | 'まるごとびわゼリー', 198 | 'ぴーなっつ最中', 199 | '千葉めぐり', 200 | 'ぴーなっつ パイ', 201 | 'ちばの穂便り', 202 | '千葉のつきと星', 203 | ], 204 | 205 | 東京都: [ 206 | '東京ばな奈', 207 | '常盤堂 人形焼', 208 | '舟和 芋ようかん', 209 | '亀十 どら焼き', 210 | 'ひよ子', 211 | 'シュガーバターサンドの木', 212 | 'とらや 羊羹', 213 | 'ごまたまご', 214 | 'あんこ玉', 215 | 'すいーとぽてたまご', 216 | ], 217 | 218 | 神奈川県: [ 219 | '黒船ハーバー', 220 | '鳩サブレー', 221 | '浜ローズ', 222 | '湘南ゴールドケーキ', 223 | 'うめ~いのし梅', 224 | 'はだのだっくわーず', 225 | '開港レシピのアップルパイ', 226 | '元祖きび餅', 227 | 'みのやの煉羊羹', 228 | '玉屋の羊かん', 229 | '鮎の塩焼きせんべい', 230 | '曽我煎餅', 231 | '松最中', 232 | '横須賀開国の香り焼きチョコ', 233 | 'オールドファッションフルーツケーキ', 234 | 'マカロン', 235 | '極上金かすてら', 236 | '本煉羊かん', 237 | '市松手毬', 238 | ], 239 | 240 | 新潟県: [ 241 | '笹餅クリーム入り', 242 | '越後笹餅', 243 | '日本海越後漬', 244 | '深山のしずく', 245 | '河川蒸気', 246 | '万代太鼓', 247 | '出陣餅', 248 | '笹雪だるま', 249 | '浮き星', 250 | '白銀サンタ', 251 | '笹だんごパン', 252 | '萬代餡', 253 | '小雪ボール', 254 | ], 255 | 256 | 富山県: [ 257 | 'まいどはや', 258 | 'おわら玉天', 259 | 'チューリップサブレ', 260 | '梨パイ', 261 | '鹿の子餅', 262 | '越中富山の売薬さん', 263 | '豆板', 264 | 'チューリップ球根菓', 265 | 'とこなつ', 266 | '月世界', 267 | ], 268 | 269 | 石川県: [ 270 | '丹波黒豆ようかん', 271 | '箔のかおり 籠入り', 272 | '五郎島金時芋まんじゅう', 273 | '珠洲焼の里', 274 | '生姜せんべい', 275 | 'おかき 海老振り', 276 | '能登大納言もなか 孤高白山', 277 | ], 278 | 279 | 福井県: [ 280 | '羽二重餅', 281 | '五月ヶ瀬 ピーナツ入せんべい', 282 | '水羊羹', 283 | 'はっくつバウム', 284 | '眼鏡堅パン', 285 | '五月ヶ瀬', 286 | '羽二重くるみ', 287 | 'から揚げせんべい', 288 | 'けんけら', 289 | 'あべかわ餅', 290 | 'ごまどうふ', 291 | '羽二重ポテト', 292 | '稲ほろり', 293 | 'ふわとろブッセ', 294 | 'バウムッシュ', 295 | '小鯛ささ漬', 296 | '焼き鯖寿司', 297 | '越前そば', 298 | '谷口屋の、おあげ', 299 | '雲丹醤', 300 | ], 301 | 302 | 山梨県: [ 303 | 'くろ玉', 304 | '桔梗信玄餅', 305 | '信玄桃', 306 | 'かねたまる餅', 307 | '信玄桃', 308 | '桔梗信玄生プリン', 309 | '桔梗信玄餅生ロール', 310 | '桔梗信玄棒', 311 | '栗せんべい', 312 | 'レーズンサンド', 313 | '生クリーム大福', 314 | '月の雫', 315 | 'フジヤマバウム', 316 | '富士の粉雪チーズケーキ', 317 | 'ふじさんプリン', 318 | '富士山羊羹', 319 | '大吟醸粕(かす)てら', 320 | '造り酒屋のあまざけ', 321 | ], 322 | 323 | 長野県: [ 324 | 'ちび栗かの子', 325 | '塩羊羹', 326 | '方寸', 327 | 'まほろばの月', 328 | 'あんず姫', 329 | 'そばおぼろ', 330 | 'りんごのささやき', 331 | ], 332 | 333 | 岐阜県: [ 334 | '栗きんとん', 335 | '黒胡麻こくせん', 336 | '柿羊羹', 337 | '鮎菓子', 338 | '水まんじゅう', 339 | '登り鮎', 340 | '本わらび餅', 341 | '飛騨の駄菓子', 342 | ], 343 | 344 | 静岡県: [ 345 | 'お茶つぶダックワーズ', 346 | 'うなぎの里', 347 | 'お茶羊羹', 348 | 'お茶物語', 349 | '昔ながらのカステラ', 350 | '茶遊里', 351 | '松かさ最中', 352 | '長八の龍', 353 | '伊豆乃踊子', 354 | '安倍川もち', 355 | 'みそ饅頭', 356 | '遠大栗', 357 | '富士のこけもも', 358 | '田子の月もなか', 359 | 'うなぎパイ', 360 | ], 361 | 362 | 愛知県: [ 363 | 'なごやん', 364 | '名古屋ひとくちバウム', 365 | '豆菓子 ピーナッツ加工豆菓子', 366 | 'きよめ餅総本家 きよめ餅', 367 | '千なり', 368 | '名古屋の殿様', 369 | '青柳ういろう', 370 | 'ひと口ういろ', 371 | 'きよめ餅', 372 | '名古屋プリン', 373 | '葵出世ロール「いざ天下取り」', 374 | 'なごやん', 375 | 'ダイナゴン', 376 | '海老夕月', 377 | '上り羊羹', 378 | '献上外良', 379 | '元祖犬山げんこつ袋入り', 380 | 'はませんえびうす焼', 381 | 'ゆかり', 382 | '平洲最中', 383 | '日月もなか', 384 | '次郎柿ゼリー', 385 | '宝珠まんじゅう', 386 | '名古屋ふらんす', 387 | '田原銘菓あさりせんべい', 388 | '手筒米菓', 389 | '金鯱まんじゅう', 390 | '犬山名物藤澤げんこつ', 391 | '古戦場最中', 392 | '海老ごころ', 393 | '豊橋カレーうどん風せんべい', 394 | '柿羊羹', 395 | '名古屋アーモンドフロランタン', 396 | 'いかの姿焼き', 397 | '豊橋名物「ゆたかおこし」', 398 | 'ももらんぐ', 399 | 'あわ雪', 400 | '名美餅', 401 | ], 402 | 403 | 三重県: [ 404 | '赤福', 405 | 'へんば餅', 406 | 'なが餅', 407 | 'シェル・レーヌ', 408 | '安永餅', 409 | '老梅', 410 | 'くひな笛', 411 | 'アイス饅頭', 412 | 'へこきまんじゅう', 413 | '絲印煎餅', 414 | 'お福餅', 415 | '平治煎餅', 416 | '蜂蜜まん', 417 | ], 418 | 419 | 滋賀県: [ 420 | 'ふくみ天平', 421 | '糸切餅', 422 | '福みたらし', 423 | 'クラブハリエ バームクーヘン', 424 | '埋れ木', 425 | '江州だんご', 426 | 'うばがもち', 427 | 'アイアシェッケ', 428 | '三井寺力餅', 429 | '叶匠壽庵 あも', 430 | '和た与 でっち羊羹', 431 | 'サラダパン', 432 | ], 433 | 434 | 京都府: [ 435 | 'おたべ', 436 | '糖蜜ボンボン 月うさぎ', 437 | '阿闍梨餅', 438 | '雪まろげ', 439 | '琥珀 柚子', 440 | 'すはまだんご', 441 | '八ツ橋', 442 | '生八つ橋黒胡麻', 443 | '松風', 444 | '五色豆', 445 | '柚子の香のそばぼうろ', 446 | '生麩まんじゅう', 447 | '千寿せんべい', 448 | '華', 449 | ], 450 | 451 | 大阪府: [ 452 | 'プレミアムみかん大福', 453 | '浪花えくぼ 粟おこし', 454 | '岩おこし', 455 | '釣鐘まんじゅう', 456 | 'いちごの王様', 457 | '搗き餅', 458 | 'けし餅', 459 | 'みたらし団子', 460 | '栗むし羊羹', 461 | 'フルーツ餅', 462 | 'カラフル白玉', 463 | 'おはぎ', 464 | '炙りみたらし', 465 | 'さつま焼き', 466 | ], 467 | 468 | 兵庫県: [ 469 | '丹波の栗どら', 470 | '銀よせようかん', 471 | '野路菊の里', 472 | '千姫さまの姫ぽてと', 473 | '炭酸せんべい', 474 | '塩味饅頭 志ほ万', 475 | '丹波黒豆きんつば', 476 | '塩味まんじゅう', 477 | '花すみれ', 478 | ], 479 | 480 | 奈良県: [ 481 | '柿けーき', 482 | '天極堂わらびもちの粉', 483 | 'しかまろくんプリントクッキー', 484 | '御城之口餅', 485 | '飛鳥の蘇', 486 | '柿もなか', 487 | 'きなこだんご', 488 | '名物みむろ最中', 489 | '埴輪まんじゅう', 490 | '鹿の角バームクーヘン', 491 | 'まほろば大仏プリン', 492 | ], 493 | 494 | 和歌山県: [ 495 | '紀州柚最中', 496 | '福菱 かげろう', 497 | '和歌山せんべい', 498 | '清四郎の炭酸煎餅', 499 | '浜木綿(はまゆう)', 500 | '柚もなか', 501 | '本ノ字饅頭', 502 | ], 503 | 504 | 鳥取県: [ 505 | '大風呂敷', 506 | '因幡の白うさぎ', 507 | '打吹公園だんご', 508 | 'ふろしきまんじゅう', 509 | 'あごちくわ', 510 | '豆腐竹輪・蒸し', 511 | '八頭ばうむ', 512 | '妖菓目玉おやじ', 513 | '二十世紀梨ゴーフレット', 514 | 'とっとり二十世紀梨わいん', 515 | ], 516 | 517 | 島根県: [ 518 | '菜種の里', 519 | '姫小袖', 520 | '山川', 521 | '柚餅子', 522 | 'どじょう掬いまんじゅう', 523 | '松江地伝酒どら焼', 524 | '山の香', 525 | '源氏巻', 526 | ], 527 | 528 | 岡山県: [ 529 | 'きびだんご', 530 | 'むらすゞめ', 531 | '松乃露', 532 | '大手まんぢゅう', 533 | '備中神楽最中', 534 | '金平饅頭', 535 | '黒豆大福', 536 | '調布', 537 | ], 538 | 539 | 広島県: [ 540 | 'やまだ屋もみじ饅頭', 541 | '蜜饅頭', 542 | '桐葉菓', 543 | '杓子せんべい', 544 | '聖乃志久礼(ひじりのしぐれ)', 545 | '御笠の老松', 546 | '生もみじ', 547 | '桐葉菓', 548 | '鳳梨萬頭・檸檬', 549 | '宮島さん', 550 | '川通り餅', 551 | '瀬戸田レモンケーキ 「島ごころ」', 552 | ], 553 | 554 | 山口県: [ 555 | '岩まん', 556 | '舌鼓', 557 | '月でひろった卵', 558 | '翁あめ', 559 | '夏蜜柑丸漬', 560 | '外郎', 561 | '幸ふくまんじゅう', 562 | '利休さん', 563 | ], 564 | 565 | 徳島県: [ 566 | '阿波ういろ', 567 | '鳴門金時 鳴門うず芋', 568 | '金長まんじゅう', 569 | '和布羊羹', 570 | 'ポテレット', 571 | 'なると金時饅頭', 572 | '小男鹿', 573 | ], 574 | 575 | 香川県: [ 576 | '茶のしずく', 577 | '和三盆きんつば', 578 | '鈴の音紀行', 579 | '直島塩クッキー', 580 | '和三盆サイダー', 581 | '梅ヶ゛枝', 582 | '名代灸まん', 583 | '栗林のくり', 584 | ], 585 | 586 | 愛媛県: [ 587 | 'ハタダ栗タルト', 588 | '坊っちゃん団子', 589 | '醤油餅', 590 | '唐饅頭黒糖', 591 | 'いよかんゼリー', 592 | 'ゆずっ子', 593 | '八幡饅頭', 594 | '山田屋まんじゅう', 595 | '母恵夢', 596 | '芋吉', 597 | '薄墨羊羹 愛媛みかん', 598 | 'みきゃんいよかんタルト', 599 | ], 600 | 601 | 高知県: [ 602 | '塩けんぴ', 603 | '土左日記', 604 | '泰作さん', 605 | '満天の星大福', 606 | 'じゅわっと半熟たまごカステラ', 607 | 'ミレービスケット', 608 | '龍馬のブーツ', 609 | 'ポテトフレークサブレ', 610 | '筏羊羹', 611 | 'かんざし', 612 | '野根まんじゅう', 613 | '梅不し', 614 | '元祖ケンピ', 615 | 'かんざしチョコ', 616 | '野根まんじゅう', 617 | '焼きたて鰹たたき', 618 | ], 619 | 620 | 福岡県: [ 621 | '鶏卵素麺', 622 | '名菓ひよ子', 623 | '千鳥饅頭', 624 | '博多の女', 625 | '鶴乃子', 626 | '筑紫もち', 627 | '雪うさぎ', 628 | 'ぎおん太鼓', 629 | '小菊饅頭', 630 | '梅ヶ枝餅', 631 | '{{博多}}通りもん', 632 | '九州ドーナツ棒 あまおう苺', 633 | ], 634 | 635 | 佐賀県: [ 636 | 'さが錦', 637 | '丸ぼうろ', 638 | '小城羊羹', 639 | '白玉饅頭', 640 | '生つつじ餅', 641 | '九州ドーナツ棒 嬉野茶', 642 | ], 643 | 644 | 長崎県: [ 645 | '茂木 一○香', 646 | 'チーズカステラ', 647 | 'チョコカステラ', 648 | '幸せのいちごカステラ', 649 | '幸せの黄色いカステラ', 650 | '長崎カステラ', 651 | '桃カステラ', 652 | '九十九島せんぺい', 653 | '小浜食糧 クルス', 654 | '長崎物語', 655 | '九州ドーナツ棒 びわ', 656 | 'しあわせクルス', 657 | '福建の よりより', 658 | 'カスドース', 659 | '文明堂総本店 レモンケーキ', 660 | '増田の丸ぼうろ', 661 | '藤田チェリー豆総本家', 662 | 'ざぼん漬', 663 | ], 664 | 665 | 熊本県: [ 666 | '誉の陣太鼓', 667 | 'いきなり団子', 668 | '武者がえし', 669 | 'こっぱもち', 670 | 'せんば小狸', 671 | '九州ドーナツ棒 栗', 672 | '九州ドーナツ棒 デコポン', 673 | '銅銭糖', 674 | ], 675 | 676 | 大分県: [ 677 | 'ざびえる', 678 | '瑠異沙(るいさ)', 679 | 'ボンディア', 680 | 'ドン・フランシスコ', 681 | '荒城の月', 682 | '塩唐揚げ煎餅', 683 | '謎のとり天せんべい', 684 | 'こまちみかん', 685 | 'やせうま', 686 | '九州ドーナツ棒 かぼす', 687 | '臼杵せんべいサブレ', 688 | '瑠異沙', 689 | '一伯', 690 | ], 691 | 692 | 宮崎県: [ 693 | '宮崎マンゴーラングドシャ', 694 | '青島せんべい', 695 | '宮崎日向夏くりーむさんど', 696 | 'チーズ饅頭', 697 | 'つきいれ餅', 698 | '昔ながらのみそせんべい', 699 | '昔ながらの生姜せんべい', 700 | 'なんじゃこら大福', 701 | ], 702 | 703 | 鹿児島県: [ 704 | 'かからん団子 よもぎ', 705 | 'あくまき', 706 | 'げたんは', 707 | '極上元かるかん', 708 | '芋吉兆', 709 | 'かすたどん', 710 | '薩摩きんつば', 711 | '九州ドーナツ棒 さつまいも', 712 | '風月堂 さつまどりサブレ', 713 | '甘えん棒 甘太郎', 714 | ], 715 | 716 | 沖縄県: [ 717 | '元祖紅いもタルト', 718 | '雪塩ちんすこう', 719 | '黒糖チョコレート', 720 | 'ちんすこうショコラ', 721 | '沖縄めんべい', 722 | 'おもろ', 723 | 'とろなまマンゴープリン', 724 | '紅芋とちんすこうのフローズンちいずケーキ', 725 | '島とうがらしえびせんべい', 726 | '謝花きっぱん', 727 | 'オリオンビアナッツ', 728 | '島唐辛子せんべい', 729 | ], 730 | }); 731 | 732 | export default sweets; 733 | -------------------------------------------------------------------------------- /src/assets/data/作業用.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /src/assets/icon/spa.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | 3 | const IconSpa: FC = () => ( 4 | 15 | 16 | 17 | 18 | 19 | 20 | ); 21 | 22 | export default IconSpa; 23 | -------------------------------------------------------------------------------- /src/assets/svg/logo-horizontal.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/assets/svg/logo-vertical.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/assets/svg/sp-bg.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/atoms/deviceicon.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | import { css } from '@emotion/react'; 3 | import { UserDevice } from 'utils/types'; 4 | import { FC } from 'react'; 5 | 6 | import { FaDesktop, FaMobileAlt, FaTabletAlt } from 'react-icons/fa'; 7 | 8 | const DeviceIcon: FC<{ 9 | device: UserDevice; 10 | }> = ({ device }) => ( 11 | 29 | {device === 'desktop' && } 30 | {device === 'tablet' && } 31 | {device === 'mobile' && } 32 | 33 | ); 34 | 35 | export default DeviceIcon; 36 | -------------------------------------------------------------------------------- /src/components/pages/privacy.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | import { css } from '@emotion/react'; 3 | import { FC, useEffect } from 'react'; 4 | import { FaTwitter } from 'react-icons/fa'; 5 | import { logPageView } from 'utils/database'; 6 | 7 | const Privacy: FC<{ toHome: () => void }> = ({ toHome }) => { 8 | const privacyComponent = css` 9 | width: 540px; 10 | margin: 60px auto 98px; 11 | animation: 0.4s ease 0s 1 normal clicked; 12 | `; 13 | const privacyHeading = css` 14 | font-size: 20px; 15 | font-weight: 900; 16 | text-align: center; 17 | margin-bottom: 12px; 18 | `; 19 | const privacyBody = css` 20 | line-height: 1.4; 21 | `; 22 | const buttons = css` 23 | display: flex; 24 | justify-content: center; 25 | margin-top: 36px; 26 | `; 27 | 28 | useEffect(() => { 29 | logPageView('privacy'); 30 | }, []); 31 | 32 | return ( 33 |
34 |

プライバシーポリシー

35 |

36 | 当サイトでは、サービス向上のため、Googleによるアクセス解析ツールGoogle 37 | Analyticsを使用しています。 38 |
39 | Google Analyticsではデータの収集のためにCookie機能を使用しています。 40 |
41 | このデータは匿名で収集されており、個人を特定するものではありません。 42 |
43 | この機能はCookieを無効にすることで収集を拒否することができますので、お使いのブラウザの設定をご確認ください。 44 |
45 | この規約に関しての詳細は 46 | 51 | Google Analyticsサービス利用規約 52 | 53 | のページや 54 | 59 | Googleポリシーと規約ページ 60 | 61 | をご覧ください。 62 |

63 |

64 | その他、要望や質問などありましたら、お気軽に 65 | 74 | 運営者のTwitter 75 | 76 | 77 | までご連絡ください。 78 |

79 |
80 | 87 |
88 |
89 | ); 90 | }; 91 | 92 | export default Privacy; 93 | -------------------------------------------------------------------------------- /src/components/templates/prizmfooter.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | import { css } from '@emotion/react'; 3 | import { FC } from 'react'; 4 | 5 | const PrizmFooter: FC<{ privacyMode: () => void }> = ({ privacyMode }) => ( 6 |
22 | 29 | 30 | 34 | 38 | 42 | 43 |

48 | © 2022{' '} 49 | 57 | koluriri 58 | 59 | . 60 |

61 | 72 |
73 | ); 74 | 75 | export default PrizmFooter; 76 | -------------------------------------------------------------------------------- /src/ducks/game.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit'; 2 | import { 3 | GameObj, 4 | initialRemain, 5 | UserSummaryObjOnStore, 6 | MessageObject, 7 | Messages, 8 | } from 'utils/types'; 9 | 10 | export type GameState = { 11 | key: string; 12 | entity: GameObj | null; 13 | isDuringGame: boolean; 14 | currentQuesIndex: number; 15 | messages: Messages; 16 | allRemains: number; 17 | summary: Partial; 18 | }; 19 | const initialState: GameState = { 20 | key: '', 21 | entity: null, 22 | isDuringGame: false, 23 | currentQuesIndex: 1, 24 | messages: [], 25 | allRemains: 1000, 26 | summary: {}, 27 | }; 28 | 29 | export const gameSlice = createSlice({ 30 | name: 'game', 31 | initialState, 32 | reducers: { 33 | setGameEntity: (state, action: PayloadAction) => { 34 | if (state.entity !== null) { 35 | return { ...state, key: '', entity: null }; 36 | } 37 | 38 | return { 39 | ...state, 40 | entity: action.payload, 41 | allRemains: action.payload.users.length * initialRemain, 42 | }; 43 | }, 44 | setGameKey: (state, action: PayloadAction) => ({ 45 | ...state, 46 | isDuringGame: true, 47 | key: action.payload, 48 | currentQuesIndex: 1, 49 | messages: [], 50 | }), 51 | startGame: (state) => ({ ...state, isDuringGame: true }), 52 | stopGame: (state) => ({ ...state, isDuringGame: false }), 53 | unsetGame: (state) => ({ ...state, key: '', entity: null }), 54 | proceedQuesIndex: (state) => ({ 55 | ...state, 56 | currentQuesIndex: state.currentQuesIndex + 1, 57 | }), 58 | pullMessage: (state, action: PayloadAction) => { 59 | if (state.messages.find((msg) => msg.key === action.payload.key)) 60 | return state; 61 | 62 | return { 63 | ...state, 64 | messages: [...state.messages, action.payload], 65 | }; 66 | }, 67 | updateSummary: (state, action: PayloadAction) => ({ 68 | ...state, 69 | summary: { ...state.summary, ...action.payload }, 70 | }), 71 | }, 72 | }); 73 | -------------------------------------------------------------------------------- /src/ducks/rootReducer.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from '@reduxjs/toolkit'; 2 | import { userSlice } from 'ducks/user'; 3 | import { gameSlice } from 'ducks/game'; 4 | 5 | const rootReducer = combineReducers({ 6 | user: userSlice.reducer, 7 | game: gameSlice.reducer, 8 | }); 9 | 10 | export type RootState = ReturnType; 11 | 12 | export default rootReducer; 13 | -------------------------------------------------------------------------------- /src/ducks/user.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit'; 2 | 3 | export const initialUserName = 'Unknown'; 4 | 5 | export type UserState = { 6 | name: string; 7 | key: string; 8 | }; 9 | const initialState: UserState = { 10 | name: initialUserName, 11 | key: '', 12 | }; 13 | 14 | export const userSlice = createSlice({ 15 | name: 'user', 16 | initialState, 17 | reducers: { 18 | setUserName: (state, action: PayloadAction) => ({ 19 | ...state, 20 | name: action.payload, 21 | }), 22 | setUserKey: (state, action: PayloadAction) => ({ 23 | ...state, 24 | key: action.payload, 25 | }), 26 | unsetUserKey: (state) => ({ ...state, key: '' }), 27 | }, 28 | }); 29 | -------------------------------------------------------------------------------- /src/hooks/use-audio.ts: -------------------------------------------------------------------------------- 1 | const useAudio = () => (path: string) => { 2 | const audio = new Audio(`/audio/${path}.mp3`); 3 | audio.volume = 0.5; 4 | audio 5 | .play() 6 | .then(() => true) 7 | .catch((error) => console.error(error)); 8 | }; 9 | 10 | export default useAudio; 11 | -------------------------------------------------------------------------------- /src/hooks/use-generates.ts: -------------------------------------------------------------------------------- 1 | const names = [ 2 | 'かに', 3 | 'きつね', 4 | 'めろん', 5 | 'りんご', 6 | 'わんこそば', 7 | 'なまはげ', 8 | 'ささかま', 9 | 'ずんだ', 10 | 'さくらんぼ', 11 | 'あかべこ', 12 | 'おこめ', 13 | 'いちご', 14 | 'なっとう', 15 | 'そば', 16 | 'せんべい', 17 | 'らっかせい', 18 | 'だいぶつ', 19 | 'ぶどう', 20 | 'ねぎ', 21 | // 'すかいつりー', 22 | 'あしたば', 23 | 'おちゃ', 24 | 'たぬき', 25 | 'みかん', 26 | 'しか', 27 | // 'おこのみやき', 28 | 'うどん', 29 | 'まいこ', 30 | 'すさのお', 31 | 'ももたろう', 32 | 'きびだんご', 33 | 'めんたいこ', 34 | 'かぼす', 35 | 'さくら', 36 | 'つる', 37 | 'かすてら', 38 | 'いか', 39 | 'まんごー', 40 | 'しーさー', 41 | 'がじゅまる', 42 | ]; 43 | 44 | export const colors = [ 45 | '#395298', 46 | '#51B1C9', 47 | '#53822B', 48 | '#6DCE97', 49 | '#76B2B4', 50 | '#8FB505', 51 | '#9F68E8', 52 | '#C26655', 53 | '#C9C21E', 54 | '#D6A671', 55 | '#D94550', 56 | '#E8A21C', 57 | '#EB5C4B', 58 | '#ED9489', 59 | '#F1B8B5', 60 | '#FFB554', 61 | ]; 62 | 63 | export const useGenerateName = () => () => 64 | names[Math.floor(Math.random() * names.length)] + 65 | Math.random().toString(16).slice(-2); 66 | 67 | export const useGenerateColor = () => () => 68 | colors[Math.floor(Math.random() * colors.length)]; 69 | -------------------------------------------------------------------------------- /src/hooks/use-getdevice.ts: -------------------------------------------------------------------------------- 1 | import { UserDevice } from 'utils/types'; 2 | 3 | type getDeviceFunction = () => UserDevice; 4 | 5 | const useGetDevice = (): getDeviceFunction => () => { 6 | const ua = navigator.userAgent; 7 | 8 | let device: UserDevice = 'desktop'; 9 | if ( 10 | ua.indexOf('iPhone') > 0 || 11 | ua.indexOf('iPod') > 0 || 12 | (ua.indexOf('Android') > 0 && ua.indexOf('Mobile') > 0) 13 | ) { 14 | device = 'mobile'; 15 | } else if (ua.indexOf('iPad') > 0 || ua.indexOf('Android') > 0) { 16 | device = 'tablet'; 17 | } 18 | 19 | return device; 20 | }; 21 | 22 | export default useGetDevice; 23 | -------------------------------------------------------------------------------- /src/hooks/use-listengameanddeleteuser.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | import { useDispatch, useSelector } from 'react-redux'; 4 | import { RootState } from 'ducks/rootReducer'; 5 | import { userSlice } from 'ducks/user'; 6 | import { gameSlice } from 'ducks/game'; 7 | 8 | import { GameObj, Mode } from 'utils/types'; 9 | import { deleteUser, listenGame } from 'utils/database'; 10 | 11 | const useListenGameAndDeleteUser = () => { 12 | const userKey = useSelector((state: RootState) => state.user.key); 13 | const dispatch = useDispatch(); 14 | const { unsetUserKey } = userSlice.actions; 15 | const { setGameKey, setGameEntity } = gameSlice.actions; 16 | 17 | const [lastMode, setLastMode] = useState('easy'); 18 | 19 | const onFocus = () => { 20 | window.location.reload(); 21 | }; 22 | const onBlur = () => { 23 | if (userKey !== '') { 24 | deleteUser(userKey); 25 | } 26 | }; 27 | 28 | const visibilitychangeCallback = () => { 29 | if (document.visibilityState === 'visible') onFocus(); 30 | if (document.visibilityState === 'hidden') onBlur(); 31 | }; 32 | 33 | useEffect(() => { 34 | if (userKey !== '') { 35 | listenGame(userKey, (data) => { 36 | if (!data.key) return false; 37 | 38 | deleteUser(userKey); 39 | dispatch(unsetUserKey()); 40 | dispatch(setGameKey(data.key)); 41 | dispatch(setGameEntity(data.val() as GameObj)); 42 | 43 | setLastMode((data.val() as GameObj).mode); 44 | 45 | return true; 46 | }); 47 | } 48 | 49 | window.addEventListener('beforeunload', onBlur); 50 | window.addEventListener('unload', onBlur); 51 | window.addEventListener('pagehide', onBlur); 52 | document.addEventListener('visibilitychange', visibilitychangeCallback); 53 | window.addEventListener('focus', onFocus); 54 | window.addEventListener('blur', onBlur); 55 | 56 | return () => { 57 | window.removeEventListener('beforeunload', onBlur); 58 | window.removeEventListener('unload', onBlur); 59 | window.removeEventListener('pagehide', onBlur); 60 | document.removeEventListener( 61 | 'visibilitychange', 62 | visibilitychangeCallback, 63 | ); 64 | window.removeEventListener('focus', onFocus); 65 | window.removeEventListener('blur', onBlur); 66 | }; 67 | // eslint-disable-next-line react-hooks/exhaustive-deps 68 | }, [userKey]); 69 | 70 | return lastMode; 71 | }; 72 | 73 | export default useListenGameAndDeleteUser; 74 | -------------------------------------------------------------------------------- /src/hooks/use-scrolldiv.ts: -------------------------------------------------------------------------------- 1 | import { RefObject, useCallback, useRef } from 'react'; 2 | 3 | const useScrollDiv = (): [RefObject, () => void] => { 4 | const viewRef = useRef(null); 5 | 6 | return [ 7 | viewRef, 8 | useCallback(() => { 9 | if (viewRef.current) 10 | viewRef.current.scrollBy({ 11 | top: viewRef.current.scrollHeight, 12 | behavior: 'smooth', 13 | }); 14 | }, [viewRef]), 15 | ]; 16 | }; 17 | 18 | export default useScrollDiv; 19 | -------------------------------------------------------------------------------- /src/hooks/use-toonline.ts: -------------------------------------------------------------------------------- 1 | import { useDispatch, useSelector } from 'react-redux'; 2 | import { RootState } from 'ducks/rootReducer'; 3 | 4 | import { initialUserName, userSlice } from 'ducks/user'; 5 | import { deleteUser, newOnlineUser } from 'utils/database'; 6 | import { 7 | localScoreKey, 8 | localUserColorKey, 9 | localUserNameKey, 10 | } from 'utils/types'; 11 | import useGetDevice from 'hooks/use-getdevice'; 12 | 13 | export const useToOnline = () => { 14 | const dispatch = useDispatch(); 15 | const { setUserKey } = userSlice.actions; 16 | 17 | const getDevice = useGetDevice(); 18 | 19 | return () => { 20 | dispatch( 21 | setUserKey( 22 | newOnlineUser({ 23 | userName: localStorage.getItem(localUserNameKey) || initialUserName, 24 | color: localStorage.getItem(localUserColorKey) || '', 25 | pingStamp: Date.now(), 26 | device: getDevice(), 27 | score: parseInt(String(localStorage.getItem(localScoreKey)), 10) || 0, 28 | }), 29 | ), 30 | ); 31 | }; 32 | }; 33 | 34 | export const useToOffline = () => { 35 | const userKey = useSelector((state: RootState) => state.user.key); 36 | const dispatch = useDispatch(); 37 | const { unsetUserKey } = userSlice.actions; 38 | 39 | return () => { 40 | deleteUser(userKey); 41 | dispatch(unsetUserKey()); 42 | }; 43 | }; 44 | -------------------------------------------------------------------------------- /src/hooks/use-username.ts: -------------------------------------------------------------------------------- 1 | import { useDispatch, useSelector } from 'react-redux'; 2 | import { RootState } from 'ducks/rootReducer'; 3 | import { userSlice, initialUserName } from 'ducks/user'; 4 | import { 5 | isUserSummaryObj, 6 | localUserColorKey, 7 | localUserNameKey, 8 | localUserSummary, 9 | } from 'utils/types'; 10 | import { useGenerateColor, useGenerateName } from 'hooks/use-generates'; 11 | 12 | const useUserName = (): string => { 13 | const userName = useSelector((state: RootState) => state.user.name); 14 | const dispatch = useDispatch(); 15 | const { setUserName } = userSlice.actions; 16 | 17 | const generateColor = useGenerateColor(); 18 | const generateName = useGenerateName(); 19 | 20 | if (userName !== initialUserName) return userName; 21 | 22 | if (!localStorage.getItem(localUserColorKey)) 23 | localStorage.setItem(localUserColorKey, generateColor()); 24 | 25 | if ( 26 | !localStorage.getItem(localUserSummary) || 27 | !isUserSummaryObj(JSON.parse(localStorage.getItem(localUserSummary) ?? '')) 28 | ) 29 | localStorage.setItem( 30 | localUserSummary, 31 | JSON.stringify({ 32 | playCount: 0, 33 | wonCount: 0, 34 | lastPlay: 0, 35 | lastWon: 0, 36 | currentStreak: 0, 37 | maxStreak: 0, 38 | averageSpeed: 0, 39 | fastestSpeed: 0, 40 | }), 41 | ); 42 | 43 | const localUserName = localStorage.getItem(localUserNameKey); 44 | if ( 45 | localUserName && 46 | localUserName !== '' && 47 | localUserName !== initialUserName 48 | ) { 49 | dispatch(setUserName(localUserName)); 50 | 51 | return localUserName; 52 | } 53 | 54 | const name = generateName(); 55 | 56 | localStorage.setItem(localUserNameKey, name); 57 | dispatch(setUserName(name)); 58 | 59 | return name; 60 | }; 61 | 62 | export default useUserName; 63 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | 4 | import { configureStore } from '@reduxjs/toolkit'; 5 | import { Provider } from 'react-redux'; 6 | import rootReducer from './ducks/rootReducer'; 7 | import 'normalize.css'; 8 | import App from './App'; 9 | import reportWebVitals from './reportWebVitals'; 10 | 11 | const store = configureStore({ reducer: rootReducer }); 12 | 13 | const root = ReactDOM.createRoot( 14 | document.getElementById('root') as HTMLElement, 15 | ); 16 | root.render( 17 | 18 | 19 | , 20 | ); 21 | 22 | // If you want to start measuring performance in your app, pass a function 23 | // to log results (for example: reportWebVitals(console.log)) 24 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 25 | reportWebVitals(); 26 | -------------------------------------------------------------------------------- /src/modules/edituser/edituser.colorselector.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | import { css } from '@emotion/react'; 3 | import { FC } from 'react'; 4 | 5 | const ColorSelector: FC<{ 6 | color: string; 7 | selected: string; 8 | onClick: () => void; 9 | }> = ({ color, selected, onClick }) => ( 10 | // eslint-disable-next-line jsx-a11y/control-has-associated-label 11 | 138 | 149 | 150 | 151 | {summary && } 152 | 153 | ); 154 | }; 155 | 156 | export default EditUser; 157 | -------------------------------------------------------------------------------- /src/modules/edituser/edituser.userpreview.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | import { css } from '@emotion/react'; 3 | import { FC } from 'react'; 4 | 5 | const UserPreview: FC<{ 6 | name: string; 7 | score: number; 8 | color: string; 9 | }> = ({ name, score, color }) => { 10 | const edituserPreview = css` 11 | margin: 0 auto 40px; 12 | padding: 5px 11px; 13 | border-radius: 20px; 14 | width: fit-content; 15 | background-color: ${color}33; 16 | font-weight: 700; 17 | font-size: 15px; 18 | display: flex; 19 | align-items: center; 20 | justify-content: center; 21 | `; 22 | const edituserPreviewIcon = css` 23 | width: 7px; 24 | height: 7px; 25 | background: ${color}; 26 | border-radius: 5px; 27 | margin-right: 5px; 28 | `; 29 | const edituserPreviewName = css` 30 | max-width: 120px; 31 | `; 32 | const edituserPreviewScore = css` 33 | font-size: 13px; 34 | font-weight: 500; 35 | margin-left: 8px; 36 | `; 37 | 38 | return ( 39 |
40 | 41 | {name.slice(0, 7)} 42 | 43 | スコア 44 | {score} 45 | 46 |
47 | ); 48 | }; 49 | 50 | export default UserPreview; 51 | -------------------------------------------------------------------------------- /src/modules/edituser/usersummary.item.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | import { css } from '@emotion/react'; 3 | import { FC } from 'react'; 4 | 5 | const UserSummaryItem: FC<{ 6 | dispNum: number | string; 7 | text: string; 8 | // eslint-disable-next-line react/require-default-props 9 | unit?: string; 10 | }> = ({ dispNum, text, unit }) => { 11 | const summaryStyle = css` 12 | display: grid; 13 | text-align: center; 14 | font-weight: bold; 15 | font-size: 13px; 16 | `; 17 | const bignumStyle = css` 18 | font-size: 40px; 19 | margin: 2px 0px; 20 | white-space: nowrap; 21 | `; 22 | const unitStyle = css` 23 | font-size: 18px; 24 | `; 25 | 26 | return ( 27 |
28 | 29 | {dispNum} 30 | {!!unit && {unit}} 31 | 32 | {text} 33 |
34 | ); 35 | }; 36 | 37 | export default UserSummaryItem; 38 | -------------------------------------------------------------------------------- /src/modules/edituser/usersummary.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | import { css } from '@emotion/react'; 3 | import { UserSummaryObj } from 'utils/types'; 4 | import { FC, useState } from 'react'; 5 | import UserSummaryItem from 'modules/edituser/usersummary.item'; 6 | 7 | const UserSummary: FC<{ summary: UserSummaryObj }> = ({ summary }) => { 8 | const colors = ['orange', 'lightbeige', 'palepink', 'pink', 'tea', 'teal']; 9 | const [bgColor] = useState(colors[Math.floor(Math.random() * colors.length)]); 10 | const userSummaryContainer = css` 11 | animation: 0.4s ease 0.2s 1 normal clicked; 12 | margin: 0 auto 80px; 13 | display: grid; 14 | grid-template-columns: repeat(auto-fit, minmax(33%, 1fr)); 15 | gap: 20px 0; 16 | padding-top: 25px; 17 | padding-bottom: 25px; 18 | border: 0; 19 | background-color: var(--${bgColor}); 20 | color: var(--black); 21 | `; 22 | 23 | return ( 24 |
25 | 26 | 27 | 28 | 37 | 42 | 47 |
48 | ); 49 | }; 50 | 51 | export default UserSummary; 52 | -------------------------------------------------------------------------------- /src/modules/game/answerinput/answerinput.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | import { css } from '@emotion/react'; 3 | import { FC, FormEvent, useEffect, useRef, useState } from 'react'; 4 | 5 | import { useSelector } from 'react-redux'; 6 | import { RootState } from 'ducks/rootReducer'; 7 | 8 | import useJudger from 'modules/game/answerinput/use-judger'; 9 | import { gameTimerSeconds, initialRemain } from 'utils/types'; 10 | import useCanonicalizePref, { 11 | canonicalizerReturn, 12 | } from 'modules/game/answerinput/use-canonicalizepref'; 13 | import useErrorMessage from 'modules/game/answerinput/use-inputerrormessage'; 14 | 15 | import UserRemain from 'modules/game/answerinput/userremain'; 16 | import InputSuggest from 'modules/game/answerinput/inputsuggest'; 17 | import InputErrorMessage from 'modules/game/answerinput/inputerrormessage'; 18 | import { FaTwitter } from 'react-icons/fa'; 19 | import { generateTweet } from 'utils/summary'; 20 | import useAudio from 'hooks/use-audio'; 21 | 22 | const AnswerInput: FC<{ 23 | setHome: () => void; 24 | }> = ({ setHome }) => { 25 | const isDuringGame = useSelector( 26 | (state: RootState) => state.game.isDuringGame, 27 | ); 28 | const gameObj = useSelector((state: RootState) => state.game.entity); 29 | const summary = useSelector((state: RootState) => state.game.summary); 30 | 31 | const [answerInputValue, setAnswerInputValue] = useState(''); 32 | const inputRef = useRef(null); 33 | useEffect(() => { 34 | setTimeout(() => { 35 | if (inputRef.current) { 36 | inputRef.current.focus(); 37 | } 38 | }, (gameTimerSeconds * 3 + 0.1) * 1000); 39 | }, []); 40 | 41 | const [errorMessage, setErrorMessage] = useErrorMessage(); 42 | 43 | const judgeAndPush = useJudger(); 44 | 45 | const [remain, setRemain] = useState(initialRemain); 46 | 47 | const canonicalizer = useCanonicalizePref(); 48 | const [[canonicalized, suggest], setCanonicalized] = 49 | useState([false, '']); 50 | 51 | const answerSubmit = (e: FormEvent) => { 52 | e.preventDefault(); 53 | if (canonicalized && canonicalized !== '') { 54 | if (remain <= 0) { 55 | setErrorMessage('残機がありません'); 56 | } else { 57 | setRemain((state) => state - 1); 58 | if (!judgeAndPush(canonicalized)) 59 | setErrorMessage(`残り${remain - 1}回`); 60 | setAnswerInputValue(''); 61 | setCanonicalized([false, suggest]); 62 | } 63 | } else { 64 | setErrorMessage('都道府県を入力してください'); 65 | } 66 | }; 67 | 68 | const isWon = summary.lastWon && summary.lastWon === gameObj?.created; 69 | 70 | const answerInputContainer = css` 71 | grid-area: answerinput; 72 | position: relative; 73 | display: flex; 74 | align-items: center; 75 | justify-content: center; 76 | ${errorMessage !== '' && 'animation: 0.4s ease 0s 1 normal blink;'} 77 | `; 78 | const inputStyle = css` 79 | width: 100%; 80 | height: 45px; 81 | display: flex; 82 | align-items: center; 83 | justify-content: center; 84 | font-size: 23px; 85 | font-weight: 900; 86 | position: absolute; 87 | padding-right: 53px; 88 | top: 0; 89 | left: 0; 90 | right: 0; 91 | bottom: 0; 92 | color: var(--black); 93 | border-color: var(--black); 94 | 95 | &::placeholder { 96 | opacity: 0.5; 97 | color: var(--black); 98 | font-weight: 500; 99 | font-size: 20px; 100 | } 101 | `; 102 | 103 | const playSE = useAudio(); 104 | 105 | return ( 106 |
107 | {isDuringGame ? ( 108 |
answerSubmit(e)}> 109 | {!!errorMessage && } 110 | 115 | 116 | { 124 | setAnswerInputValue(e.target.value); 125 | setCanonicalized(canonicalizer(e.target.value)); 126 | }} 127 | /> 128 | 129 | ) : ( 130 | <> 131 | {isWon && ( 132 | 140 | ツイート 141 | 146 | 147 | )} 148 | 170 | 171 | )} 172 |
173 | ); 174 | }; 175 | 176 | export default AnswerInput; 177 | -------------------------------------------------------------------------------- /src/modules/game/answerinput/inputerrormessage.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | import { css } from '@emotion/react'; 3 | import { FC } from 'react'; 4 | 5 | const InputErrorMessage: FC<{ 6 | errorMessage: string; 7 | }> = ({ errorMessage }) => ( 8 |

24 | {errorMessage} 25 |

26 | ); 27 | 28 | export default InputErrorMessage; 29 | -------------------------------------------------------------------------------- /src/modules/game/answerinput/inputsuggest.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | import { css } from '@emotion/react'; 3 | import { FC } from 'react'; 4 | 5 | const InputSuggest: FC<{ 6 | canonicalized: string | false; 7 | answerInputValue: string; 8 | suggest: string; 9 | }> = ({ canonicalized, answerInputValue, suggest }) => { 10 | const suggestContainer = css` 11 | position: absolute; 12 | pointer-events: none; 13 | font-weight: 500; 14 | font-size: 23px; 15 | opacity: 0.5; 16 | display: flex; 17 | align-items: center; 18 | padding: 0 19px; 19 | margin: 3px; 20 | line-height: 1; 21 | white-space: nowrap; 22 | height: 45px; 23 | vertical-align: middle; 24 | top: 0; 25 | left: 0; 26 | right: 0; 27 | bottom: 0; 28 | width: 100%; 29 | color: var(--black); 30 | /*width: calc(100% - 53px);*/ 31 | overflow: hidden; 32 | border-radius: 23px; 33 | padding-right: 53px; 34 | `; 35 | const suggestValue = css` 36 | font-weight: 500; 37 | opacity: 0; 38 | `; 39 | const suggestCaption = css` 40 | font-size: 18px; 41 | `; 42 | 43 | return ( 44 |

45 | {canonicalized && ( 46 | <> 47 | {answerInputValue} 48 | {suggest.slice(answerInputValue.length)} 49 | (改行で送信) 50 | 51 | )} 52 |

53 | ); 54 | }; 55 | 56 | export default InputSuggest; 57 | -------------------------------------------------------------------------------- /src/modules/game/answerinput/use-canonicalizepref.ts: -------------------------------------------------------------------------------- 1 | const variationPref: { 2 | [pref: string]: string[]; 3 | } = { 4 | 北海道: ['北海道', 'ほっかいどう', 'hokkaidou', 'hkd北海道'], 5 | 青森県: ['青森県', 'あおもり', 'aomori', 'amr青森', 'aomr青森'], 6 | 岩手県: ['岩手県', 'いわて', 'iwate', 'iwt岩手'], 7 | 宮城県: ['宮城県', 'みやぎ', 'miyagi', 'myg宮城'], 8 | 秋田県: ['秋田県', 'あきた', 'akita', 'akt秋田'], 9 | 山形県: ['山形県', 'やまがた', 'yamagata', 'ymgt山形'], 10 | 福島県: [ 11 | '福島県', 12 | 'ふくしま', 13 | 'fukushima', 14 | 'hukushima', 15 | 'fukusima', 16 | 'hukusima', 17 | ], 18 | 茨城県: ['茨城県', 'いばらき', 'ibaraki', 'ibrk茨城'], 19 | 栃木県: ['栃木県', 'とちぎ', 'totigi', 'tochigi', 'tcg栃木', 'ttg栃木'], 20 | 群馬県: ['群馬県', 'ぐんま', 'gunma', 'gunnma', 'gnm群馬'], 21 | 埼玉県: ['埼玉県', 'さいたま', 'saitama', 'stm埼玉', 'sitm埼玉'], 22 | 千葉県: ['千葉県', 'ちば', 'chiba', 'tiba', 'tb千葉', 'cb千葉', 'chb千葉'], 23 | 東京都: ['東京都', 'とうきょう', 'toukyou', 'toukilyou', 'tokyo', 'tk東京'], 24 | 神奈川県: ['神奈川県', 'かながわ', 'kanagawa', 'kngw神奈川'], 25 | 新潟県: ['新潟県', 'にいがた', 'niigata', 'ngt新潟', 'nigt新潟'], 26 | 富山県: ['富山県', 'とやま', 'toyama', 'tym富山'], 27 | 石川県: ['石川県', 'いしかわ', 'ishikawa', 'isikawa', 'iskw石川'], 28 | 福井県: ['福井県', 'ふくい', 'fukui', 'hukui', 'fki福井', 'hki福井'], 29 | 山梨県: ['山梨県', 'やまなし', 'yamanasi', 'yamanashi', 'ymns山梨'], 30 | 長野県: ['長野県', 'ながの', 'nagano', 'ngn長野'], 31 | 岐阜県: ['岐阜県', 'ぎふ', 'gihu', 'gifu', 'gf岐阜', 'gh岐阜'], 32 | 静岡県: ['静岡県', 'しずおか', 'sizuoka', 'shizuoka', 'szok静岡'], 33 | 愛知県: ['愛知県', 'あいち', 'aichi', 'aiti', 'ac愛知', 'at愛知', 'aic愛知'], 34 | 三重県: ['三重県', 'みえ', 'mie'], 35 | 滋賀県: ['滋賀県', 'しが', 'siga', 'shiga', 'sig滋賀'], 36 | 京都府: ['京都府', 'きょうと', 'kyouto', 'kyoto', 'kto京都'], 37 | 大阪府: ['大阪府', 'おおさか', 'oosaka', 'osaka', 'osk大阪'], 38 | 兵庫県: ['兵庫県', 'ひょうご', 'hyougo', 'hilyougo', 'hg兵庫', 'hyg兵庫'], 39 | 奈良県: ['奈良県', 'なら', 'nara', 'nr奈良'], 40 | 和歌山県: ['和歌山県', 'わかやま', 'wakayama', 'wkym和歌山'], 41 | 鳥取県: ['鳥取県', 'とっとり', 'tottori', 'toltutori', 'ttr鳥取'], 42 | 島根県: ['島根県', 'しまね', 'simane', 'shimane', 'smn島根'], 43 | 岡山県: ['岡山県', 'おかやま', 'okayama', 'okym岡山'], 44 | 広島県: ['広島県', 'ひろしま', 'hirosima', 'hiroshima', 'hrsm広島'], 45 | 山口県: [ 46 | '山口県', 47 | 'やまぐち', 48 | 'yamaguti', 49 | 'yamaguchi', 50 | 'ymgc山口', 51 | 'ymgut山口', 52 | ], 53 | 徳島県: ['徳島県', 'とくしま', 'tokusima', 'tokushima', 'tksm徳島'], 54 | 香川県: ['香川県', 'かがわ', 'かかがわ', 'kagawa', 'kgw香川'], 55 | 愛媛県: ['愛媛県', 'えひめ', 'ehime', 'ehm愛媛'], 56 | 高知県: ['高知県', 'こうち', 'kouchi', 'kouti', 'kti高知'], 57 | 福岡県: ['福岡県', 'ふくおか', 'fukuoka', 'hukuoka', 'fkok福岡', 'hkok福岡'], 58 | 佐賀県: ['佐賀県', 'さが', 'saga', 'sag佐賀'], 59 | 長崎県: ['長崎県', 'ながさき', 'nagasaki', 'ngsk長崎'], 60 | 熊本県: ['熊本県', 'くまもと', 'kumamoto', 'kmmt熊本', 'kmt熊本'], 61 | 大分県: ['大分県', 'おおいた', 'ooita', 'oita', 'oit大分'], 62 | 宮崎県: ['宮崎県', 'みやざき', 'miyazaki', 'myzk宮崎'], 63 | 鹿児島県: ['鹿児島県', 'かごしま', 'kagosima', 'kagoshima', 'kgsm鹿児島'], 64 | 沖縄県: ['沖縄県', 'おきなわ', 'okinawa', 'oknw沖縄'], 65 | }; 66 | 67 | export type canonicalizerReturn = [ 68 | canonicalized: string | false, 69 | suggest: string, 70 | ]; 71 | export type canonicalizerFunction = (input: string) => canonicalizerReturn; 72 | 73 | const useCanonicalizePref = 74 | (): canonicalizerFunction => 75 | (input: string): canonicalizerReturn => { 76 | let canonicalizedInput: string = input 77 | .replace(/[!-~]/g, (s) => String.fromCharCode(s.charCodeAt(0) - 0xfee0)) 78 | .toLowerCase(); 79 | if (/^[\u3040-\u309F]/gu.test(canonicalizedInput)) { 80 | canonicalizedInput = canonicalizedInput.replace(/[a-zA-z]/g, ''); 81 | } 82 | let suggest = ''; 83 | 84 | const filtered = Object.keys(variationPref).filter((pref) => 85 | variationPref[pref].find((various: string) => { 86 | if (various.startsWith(canonicalizedInput)) { 87 | suggest = various; 88 | 89 | return true; 90 | } 91 | 92 | return false; 93 | }), 94 | ); 95 | if (filtered.length === 1) return [filtered[0], suggest]; 96 | 97 | return [false, suggest]; 98 | }; 99 | 100 | export default useCanonicalizePref; 101 | -------------------------------------------------------------------------------- /src/modules/game/answerinput/use-inputerrormessage.ts: -------------------------------------------------------------------------------- 1 | import { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react'; 2 | 3 | const useErrorMessage = ( 4 | microsecond = 1500, 5 | ): [string, Dispatch>] => { 6 | const [errorMessage, setErrorMessage] = useState(''); 7 | const timerId = useRef(); 8 | const clearTimer = () => clearTimeout(timerId.current); 9 | useEffect(() => { 10 | if (errorMessage !== '') { 11 | clearTimer(); 12 | timerId.current = setTimeout(() => { 13 | setErrorMessage(''); 14 | }, microsecond); 15 | } 16 | }, [errorMessage, microsecond]); 17 | 18 | return [errorMessage, setErrorMessage]; 19 | }; 20 | 21 | export default useErrorMessage; 22 | -------------------------------------------------------------------------------- /src/modules/game/answerinput/use-judger.ts: -------------------------------------------------------------------------------- 1 | import { useDispatch, useSelector } from 'react-redux'; 2 | import { RootState } from 'ducks/rootReducer'; 3 | import { gameSlice } from 'ducks/game'; 4 | 5 | import { deleteGame, logMatched, pushMessage } from 'utils/database'; 6 | import { localUserNameKey } from 'utils/types'; 7 | import getHint from 'utils/gethint'; 8 | import { initialUserName } from 'ducks/user'; 9 | import { getNoticesWhenMatched, getSummary } from 'utils/summary'; 10 | import useUserScore from 'modules/game/answerinput/use-userscore'; 11 | 12 | const useJudger = (): ((inputValue: string) => boolean) => { 13 | const userName = localStorage.getItem(localUserNameKey) || initialUserName; 14 | 15 | const gameKey = useSelector((state: RootState) => state.game.key); 16 | const gameObj = useSelector((state: RootState) => state.game.entity); 17 | const gameAnswer = gameObj?.answer; 18 | const currentQuesIndex = useSelector( 19 | (state: RootState) => state.game.currentQuesIndex, 20 | ); 21 | const dispatch = useDispatch(); 22 | const { updateSummary } = gameSlice.actions; 23 | 24 | const messages = useSelector((state: RootState) => state.game.messages); 25 | const allRemains = useSelector((state: RootState) => state.game.allRemains); 26 | 27 | const updateScore = useUserScore(); 28 | 29 | const judge = ( 30 | inputValue: string, 31 | ): [isMatched: boolean, hintMessage: string, isEnd: boolean] => { 32 | const isMatched = gameAnswer === inputValue; 33 | 34 | if (!isMatched) { 35 | const answerLength = 36 | messages.filter( 37 | (message) => message.type === 'answer' && message.matched === false, 38 | ).length + 1; 39 | 40 | if (answerLength === allRemains) { 41 | deleteGame(gameKey); 42 | 43 | return [isMatched, '', true]; 44 | } 45 | 46 | return [ 47 | isMatched, 48 | getHint((answerLength / allRemains) * 100, gameAnswer), 49 | false, 50 | ]; 51 | } 52 | 53 | return [isMatched, '', false]; 54 | }; 55 | 56 | const judgeAndPush = (inputValue: string) => { 57 | const [isMatched, hintMessage, end] = judge(inputValue); 58 | 59 | pushMessage(gameKey, { 60 | name: userName, 61 | type: 'answer', 62 | matched: isMatched, 63 | value: inputValue, 64 | }); 65 | if (hintMessage !== '') 66 | pushMessage(gameKey, { type: 'hint', value: hintMessage }); 67 | if (end) { 68 | pushMessage(gameKey, { 69 | type: 'end', 70 | value: '誰も答えられませんでした', 71 | }); 72 | } 73 | 74 | if (gameObj && isMatched) { 75 | const notice = { 76 | a_score: updateScore( 77 | gameObj.mode, 78 | gameObj.questions.length, 79 | currentQuesIndex, 80 | ), 81 | ...getNoticesWhenMatched(gameObj.created), 82 | }; 83 | 84 | const summary = getSummary(); 85 | if (summary) dispatch(updateSummary({ ...summary, ...notice })); 86 | 87 | logMatched(notice); 88 | pushMessage(gameKey, { 89 | type: 'score', 90 | value: `${userName}:`, 91 | notice, 92 | }); 93 | deleteGame(gameKey); 94 | } 95 | 96 | return isMatched; 97 | }; 98 | 99 | return judgeAndPush; 100 | }; 101 | 102 | export default useJudger; 103 | -------------------------------------------------------------------------------- /src/modules/game/answerinput/use-userscore.ts: -------------------------------------------------------------------------------- 1 | import { localScoreKey, Mode, modesScore } from 'utils/types'; 2 | 3 | type UpdateScoreFunction = ( 4 | mode: Mode, 5 | questionsLength: number, 6 | current: number, 7 | ) => number; 8 | 9 | const useUserScore = 10 | (): UpdateScoreFunction => 11 | (mode: Mode, questionsLength: number, current: number) => { 12 | const score = Math.round( 13 | modesScore[mode](100 - (100 / questionsLength) * (current - 1)), 14 | ); 15 | 16 | // local storageに加算 17 | const localScore = localStorage.getItem(localScoreKey); 18 | const setValue = String( 19 | localScore ? parseInt(localScore, 10) + score : score, 20 | ); 21 | localStorage.setItem(localScoreKey, setValue); 22 | 23 | return score; 24 | }; 25 | 26 | export default useUserScore; 27 | -------------------------------------------------------------------------------- /src/modules/game/answerinput/userremain.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | import { css } from '@emotion/react'; 3 | import { FC } from 'react'; 4 | 5 | import { FaHeart, FaTimes } from 'react-icons/fa'; 6 | 7 | const UserRemain: FC<{ 8 | remain: number; 9 | }> = ({ remain }) => ( 10 |
24 | 25 | 26 | {remain} 27 |
28 | ); 29 | 30 | export default UserRemain; 31 | -------------------------------------------------------------------------------- /src/modules/game/chat/chatcontainer.chat.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | import { css } from '@emotion/react'; 3 | import { FC, RefObject } from 'react'; 4 | 5 | import { useSelector } from 'react-redux'; 6 | import { RootState } from 'ducks/rootReducer'; 7 | 8 | import { MessageObject } from 'utils/types'; 9 | import Message from 'modules/game/chat/message'; 10 | 11 | const Chat: FC<{ 12 | chatView: RefObject; 13 | }> = ({ chatView }) => { 14 | const isDuringGame = useSelector( 15 | (state: RootState) => state.game.isDuringGame, 16 | ); 17 | const gameColor = useSelector( 18 | (state: RootState) => state.game.entity?.color ?? 'var(--bg-color)', 19 | ); 20 | const messages = useSelector((state: RootState) => state.game.messages); 21 | 22 | const chatContainer = css` 23 | grid-area: chat; 24 | overflow-y: auto; 25 | margin-bottom: 18px; 26 | display: flex; 27 | flex-direction: column; 28 | align-items: flex-end; 29 | flex-flow: column nowrap; 30 | padding-top: 80px; 31 | scrollbar-width: none; 32 | 33 | &::-webkit-scrollbar { 34 | display: none; 35 | } 36 | 37 | &::before { 38 | content: ''; 39 | position: absolute; 40 | /* top: 0; */ 41 | background: linear-gradient( 42 | 0deg, 43 | transparent, 44 | ${isDuringGame ? gameColor : 'var(--bg-color)'} 45 | ); 46 | height: 80px; 47 | display: block; 48 | width: 140px; 49 | transform: translateY(-80px); 50 | transition: 0.2s; 51 | animation: 1s ease 0s 1 normal fadein; 52 | } 53 | 54 | & > :first-of-type { 55 | margin-top: auto !important; 56 | } 57 | `; 58 | 59 | return ( 60 |
66 | {messages.map((message: MessageObject) => ( 67 | 68 | ))} 69 |
70 | ); 71 | }; 72 | 73 | export default Chat; 74 | -------------------------------------------------------------------------------- /src/modules/game/chat/chatcontainer.matchedtext.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | import { css } from '@emotion/react'; 3 | import { Messages } from 'utils/types'; 4 | import { FC } from 'react'; 5 | 6 | const MatchedText: FC<{ 7 | messages: Messages; 8 | }> = ({ messages }) => { 9 | const matchedName = messages.find( 10 | (msg) => msg.type === 'answer' && msg.matched, 11 | ); 12 | 13 | return ( 14 |
15 | 21 | {matchedName?.type === 'answer' && matchedName.name} 22 | 23 |
24 | あたり! 25 |
26 | ); 27 | }; 28 | 29 | export default MatchedText; 30 | -------------------------------------------------------------------------------- /src/modules/game/chat/chatcontainer.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useEffect } from 'react'; 2 | 3 | import { useSelector, useDispatch } from 'react-redux'; 4 | import { RootState } from 'ducks/rootReducer'; 5 | import { gameSlice } from 'ducks/game'; 6 | import { listenMessage } from 'utils/database'; 7 | 8 | import useScrollDiv from 'hooks/use-scrolldiv'; 9 | import useMatchedAnimation from 'modules/game/chat/use-matchedanimation'; 10 | 11 | import MatchedText from 'modules/game/chat/chatcontainer.matchedtext'; 12 | import Chat from 'modules/game/chat/chatcontainer.chat'; 13 | 14 | const ChatContainer: FC = () => { 15 | const dispatch = useDispatch(); 16 | const { pullMessage } = gameSlice.actions; 17 | 18 | const gameKey = useSelector((state: RootState) => state.game.key); 19 | const gameColor = useSelector( 20 | (state: RootState) => state.game.entity?.color ?? 'var(--bg-color)', 21 | ); 22 | const isDuringGame = useSelector( 23 | (state: RootState) => state.game.isDuringGame, 24 | ); 25 | const messages = useSelector((state: RootState) => state.game.messages); 26 | 27 | const [chatView, scrollChat] = useScrollDiv(); 28 | const matchedAnimation = useMatchedAnimation(gameColor, scrollChat); 29 | 30 | useEffect(() => { 31 | listenMessage(gameKey, (message) => { 32 | if (message.type === 'hint') { 33 | setTimeout(() => scrollChat(), 500); 34 | } 35 | if (message.type === 'answer' && message.matched) { 36 | matchedAnimation(); 37 | } 38 | 39 | dispatch(pullMessage(message)); 40 | setTimeout(() => scrollChat(), 150); 41 | }); 42 | }, [gameKey, matchedAnimation, scrollChat, dispatch, pullMessage]); 43 | 44 | useEffect(() => { 45 | if (!isDuringGame) setTimeout(() => scrollChat(), 400); 46 | }, [isDuringGame, scrollChat]); 47 | 48 | return ( 49 | <> 50 | 51 | 52 | 53 | ); 54 | }; 55 | 56 | export default ChatContainer; 57 | -------------------------------------------------------------------------------- /src/modules/game/chat/message.content.scores.notice.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | import { css } from '@emotion/react'; 3 | import { FC, ReactNode } from 'react'; 4 | 5 | const MessageNotice: FC<{ 6 | animationDelay: number; 7 | background: string; 8 | children: ReactNode; 9 | }> = ({ animationDelay, background, children }) => ( 10 | 18 | {children} 19 | 20 | ); 21 | 22 | export default MessageNotice; 23 | -------------------------------------------------------------------------------- /src/modules/game/chat/message.content.scores.tsx: -------------------------------------------------------------------------------- 1 | import MessageNotice from 'modules/game/chat/message.content.scores.notice'; 2 | import { MessageNoticeObj } from 'utils/types'; 3 | import { FC } from 'react'; 4 | 5 | const MessageScores: FC<{ 6 | notice: MessageNoticeObj; 7 | }> = ({ notice }) => ( 8 | <> 9 | {Object.keys(notice).map((noticeType) => { 10 | switch (noticeType) { 11 | case 'a_score': 12 | return ( 13 | 18 | スコア +{notice && notice[noticeType]} 19 | 20 | ); 21 | break; 22 | 23 | case 'b_update_fastest': 24 | return ( 25 | 30 | {notice && notice[noticeType]} 31 | 秒で回答 32 |
33 | 最速記録更新 34 |
35 | ); 36 | break; 37 | 38 | case 'c_update_streak': 39 | return ( 40 | 45 | {notice && notice[noticeType]}連勝中 46 | 47 | ); 48 | break; 49 | 50 | case 'd_update_max_streak': 51 | return ( 52 | notice && 53 | notice.d_update_max_streak && 54 | notice.d_update_max_streak !== 1 && ( 55 | 60 | 連勝記録更新 61 | 62 | ) 63 | ); 64 | break; 65 | 66 | default: 67 | return true; 68 | break; 69 | } 70 | })} 71 | 72 | ); 73 | 74 | export default MessageScores; 75 | -------------------------------------------------------------------------------- /src/modules/game/chat/message.content.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | import { css } from '@emotion/react'; 3 | 4 | import { MessageObject } from 'utils/types'; 5 | import { FC, ReactNode } from 'react'; 6 | 7 | import { useSelector } from 'react-redux'; 8 | import { RootState } from 'ducks/rootReducer'; 9 | 10 | import { FaTimes } from 'react-icons/fa'; 11 | import MessageScores from 'modules/game/chat/message.content.scores'; 12 | 13 | const MessageContent: FC<{ 14 | message: MessageObject; 15 | children: ReactNode; 16 | }> = ({ message, children }) => { 17 | const gameColor = useSelector( 18 | (state: RootState) => state.game.entity?.color ?? 'var(--bg-color)', 19 | ); 20 | 21 | const messageContent = css` 22 | ${message.type === 'answer' && 23 | !message.matched && 24 | 'animation: 0.4s ease 0s 1 normal redblink;'} 25 | color: var(--message-color); 26 | ${message.type !== 'answer' && 'color: var(--black);'} 27 | border-color: var(--message-color) !important; 28 | `; 29 | const hintHeading = css` 30 | color: ${gameColor}; 31 | `; 32 | const matchIconStyle = css` 33 | margin-right: 4px; 34 | `; 35 | 36 | return ( 37 |
38 | {message.type === 'hint' && ( 39 | 40 | ヒント 41 |
42 |
43 | )} 44 | 45 | {message.type === 'answer' && 46 | (message.matched ? ( 47 | 48 | ) : ( 49 | 50 | ))} 51 | 52 | {children} 53 | 54 | {message.type === 'score' && message.notice && ( 55 | 56 | )} 57 |
58 | ); 59 | }; 60 | 61 | export default MessageContent; 62 | -------------------------------------------------------------------------------- /src/modules/game/chat/message.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | import { css } from '@emotion/react'; 3 | import { FC, useCallback, useEffect, useState } from 'react'; 4 | 5 | import { MessageObject } from 'utils/types'; 6 | import MessageContent from 'modules/game/chat/message.content'; 7 | import useAudio from 'hooks/use-audio'; 8 | 9 | const Message: FC<{ 10 | message: MessageObject; 11 | }> = ({ message }) => { 12 | const isDisabled = 13 | (message.type === 'answer' && !message.matched) || 14 | (message.type !== 'answer' && 15 | message.type !== 'score' && 16 | message.type !== 'end'); 17 | 18 | const messageContainer = css` 19 | color: var(--message-color); 20 | display: flex; 21 | justify-content: center; 22 | align-items: end; 23 | flex-direction: column; 24 | margin: 8px 0; 25 | transition: 0.2s, visibility 0s; 26 | ${message.type === 'answer' && message.matched 27 | ? 'transform-origin: center right;animation: 0.3s ease 0s 1 normal message-added-matched;' 28 | : 'transform-origin: bottom right;animation: 0.2s ease 0s 1 normal message-added;'} 29 | `; 30 | const messageName = css` 31 | font-size: 13px; 32 | font-weight: 700; 33 | margin-right: 23px; 34 | `; 35 | 36 | const playSE = useAudio(); 37 | const [isPlayed, setIsPlayed] = useState(false); 38 | 39 | const playSEOnce = useCallback( 40 | (path: string) => { 41 | if (!isPlayed) { 42 | playSE(path); 43 | setIsPlayed(true); 44 | } 45 | }, 46 | [isPlayed, playSE], 47 | ); 48 | 49 | useEffect(() => { 50 | switch (message.type) { 51 | case 'start': 52 | playSEOnce('start'); 53 | break; 54 | 55 | case 'hint': 56 | setTimeout(() => { 57 | playSEOnce('hint'); 58 | }, 400); 59 | break; 60 | 61 | case 'answer': 62 | if (message.matched) { 63 | playSEOnce('correct'); 64 | } else { 65 | playSEOnce('incorrect'); 66 | } 67 | break; 68 | 69 | case 'end': 70 | if (message.value.indexOf('中止') !== -1) { 71 | playSEOnce('cancel'); 72 | } else { 73 | playSEOnce('end'); 74 | } 75 | break; 76 | 77 | default: 78 | break; 79 | } 80 | }, [message, playSEOnce]); 81 | 82 | return ( 83 |
90 | {message.type === 'answer' && ( 91 | {message.name} 92 | )} 93 | 94 | {message.value 95 | .split(/(\n)/g) 96 | // eslint-disable-next-line react/no-array-index-key 97 | .map((t, index) => (t === '\n' ?
: t))} 98 |
99 |
100 | ); 101 | }; 102 | 103 | export default Message; 104 | -------------------------------------------------------------------------------- /src/modules/game/chat/use-matchedanimation.ts: -------------------------------------------------------------------------------- 1 | const useMatchedAnimation = 2 | (gameColor: string, scrollChat: () => void) => () => { 3 | document.body.classList.add('matched'); 4 | 5 | setTimeout(() => { 6 | document.body.style.backgroundColor = gameColor; 7 | document 8 | .querySelector("meta[name='theme-color']") 9 | ?.setAttribute('content', gameColor); 10 | }, 10); 11 | 12 | setTimeout(() => { 13 | document.body.classList.remove('matched'); 14 | document.body.style.backgroundColor = 'var(--bg-color)'; 15 | document 16 | .querySelector("meta[name='theme-color']") 17 | ?.setAttribute('content', '#f2efe2'); 18 | document 19 | .querySelector("meta[name='theme-color'][media*='dark']") 20 | ?.setAttribute('content', '#0b141c'); 21 | }, 800); 22 | 23 | setTimeout(() => scrollChat(), 900); 24 | }; 25 | 26 | export default useMatchedAnimation; 27 | -------------------------------------------------------------------------------- /src/modules/game/game.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | import { css } from '@emotion/react'; 3 | import { FC } from 'react'; 4 | 5 | import { useSelector } from 'react-redux'; 6 | import { RootState } from 'ducks/rootReducer'; 7 | 8 | import Questioner from 'modules/game/questioner/questioner'; 9 | import Chat from 'modules/game/chat/chatcontainer'; 10 | import AnswerInput from 'modules/game/answerinput/answerinput'; 11 | 12 | import useFitScreenHeight from 'modules/game/use-fitScreenHeight'; 13 | import useGameStarted from 'modules/game/use-gamestarted'; 14 | 15 | const Game: FC<{ 16 | setHome: () => void; 17 | }> = ({ setHome }) => { 18 | const isDuringGame = useSelector( 19 | (state: RootState) => state.game.isDuringGame, 20 | ); 21 | 22 | const finishGame = useGameStarted(); 23 | 24 | const gameHeight = useFitScreenHeight(); 25 | const gameWrapper = css` 26 | height: ${gameHeight}px; 27 | display: flex; 28 | justify-content: center; 29 | align-items: center; 30 | --message-color: ${isDuringGame ? 'var(--black)' : 'var(--primary-color)'}; 31 | `; 32 | const gameContent = css` 33 | height: ${gameHeight - 20}px; 34 | max-height: 600px; 35 | padding: 0 30px; 36 | width: 730px; 37 | max-width: 100vw; 38 | display: grid; 39 | grid-template-columns: 1fr minmax(140px, 40%); 40 | grid-template-rows: 1fr ${isDuringGame ? '49px' : '118px'}; 41 | grid-template-areas: 42 | 'questioner chat' 43 | 'answerinput answerinput'; 44 | 45 | ${isDuringGame && 46 | css` 47 | @media (min-width: 768px) { 48 | grid-template-areas: 49 | 'questioner chat' 50 | 'questioner answerinput'; 51 | } 52 | `} 53 | `; 54 | 55 | return ( 56 |
57 |
58 | finishGame()} /> 59 | 60 | 61 |
62 |
63 | ); 64 | }; 65 | 66 | export default Game; 67 | -------------------------------------------------------------------------------- /src/modules/game/questioner/answerdisplay.pref3d.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | import { css, keyframes } from '@emotion/react'; 3 | import { PrefectureStr } from 'utils/types'; 4 | import { FC } from 'react'; 5 | 6 | import Prefsvg from 'modules/game/questioner/answerdisplay.pref3d.svg'; 7 | import Ztext from 'react-ztext'; 8 | 9 | const Pref3D: FC<{ 10 | name: PrefectureStr; 11 | }> = ({ name }) => ( 12 |
div > span { 21 | animation: 8s linear 0s infinite alternate ${keyframes` 22 | 0% { 23 | transform: rotateY(-50deg); 24 | } 25 | 100% { 26 | transform: rotateY(50deg); 27 | } 28 | `}; 29 | } 30 | 31 | & > div > span > span:last-of-type { 32 | filter: drop-shadow(3px 10px 16px rgba(50, 8, 10, 0.1)); 33 | } 34 | 35 | & > div > span > span:not(:first-of-type) { 36 | filter: brightness(0.72); 37 | } 38 | 39 | & svg { 40 | max-height: 26vh; 41 | width: 100%; 42 | padding: 10px; 43 | 44 | & > *:not(defs) { 45 | animation: 1s ease 0.7s 1 both ${keyframes` 46 | 0% { 47 | opacity:0; 48 | transform: translateY(220px); 49 | } 50 | 1% { 51 | opacity:1; 52 | } 53 | 45% { 54 | transform: translateY(-70px); 55 | } 56 | 55% { 57 | transform: translateY(0); 58 | } 59 | 75% { 60 | transform: translateY(-25px); 61 | } 62 | 85% { 63 | transform: translateY(0); 64 | } 65 | 93% { 66 | transform: translateY(-10px); 67 | } 68 | 100% { 69 | transform: translateY(0); 70 | } 71 | `}; 72 | 73 | &:nth-of-type(2n) { 74 | animation-delay: 0.8s; 75 | transform: translateZ(2000px); 76 | } 77 | &:nth-of-type(5n) { 78 | animation-delay: 0.9s; 79 | } 80 | } 81 | } 82 | `} 83 | > 84 | 94 | 95 | 96 |
97 | ); 98 | 99 | export default Pref3D; 100 | -------------------------------------------------------------------------------- /src/modules/game/questioner/answerdisplay.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | import { css } from '@emotion/react'; 3 | import { prefectureABC } from 'assets/data/prefecture'; 4 | import { PrefectureStr } from 'utils/types'; 5 | import { FC } from 'react'; 6 | 7 | import Pref3D from 'modules/game/questioner/answerdisplay.pref3d'; 8 | 9 | const AnswerDisplay: FC<{ 10 | answer: PrefectureStr; 11 | color: string; 12 | }> = ({ answer, color }) => { 13 | const answerDisp = css` 14 | font-size: 38px; 15 | text-align: center; 16 | letter-spacing: -1px; 17 | font-weight: 700; 18 | margin-top: 0; 19 | margin-bottom: 10px; 20 | 21 | @media (max-width: 374px) { 22 | font-size: 28px; 23 | } 24 | 25 | @media (min-width: 768px) { 26 | letter-spacing: 1px; 27 | } 28 | `; 29 | const answerEngDisp = css` 30 | display: block; 31 | font-weight: 700; 32 | font-size: 13px; 33 | letter-spacing: 4px; 34 | margin-top: 5px; 35 | color: ${color}; 36 | `; 37 | 38 | return ( 39 | <> 40 |

41 | {answer} 42 | {prefectureABC[answer]} 43 |

44 | 45 | 46 | ); 47 | }; 48 | 49 | export default AnswerDisplay; 50 | -------------------------------------------------------------------------------- /src/modules/game/questioner/bigquestion.circle.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/exhaustive-deps */ 2 | /** @jsxImportSource @emotion/react */ 3 | import { css, keyframes } from '@emotion/react'; 4 | import { FC, useMemo } from 'react'; 5 | import { useSelector } from 'react-redux'; 6 | import { RootState } from 'ducks/rootReducer'; 7 | 8 | const BigQuestionCircle: FC<{ 9 | size: number; 10 | }> = ({ size }) => { 11 | const gameColor = useSelector( 12 | (state: RootState) => state.game.entity?.color ?? 'var(--bg-color)', 13 | ); 14 | const currentQuesIndex = useSelector( 15 | (state: RootState) => state.game.currentQuesIndex, 16 | ); 17 | 18 | const circleBg = currentQuesIndex > 3 ? 'var(--red)' : gameColor; 19 | const circleEngaged = currentQuesIndex > 3 ? 'var(--black)' : 'var(--red)'; 20 | 21 | const animation = useMemo( 22 | () => css` 23 | animation: 1.5s linear 0s 1 normal ${keyframes` 24 | 0% { 25 | font-size: ${currentQuesIndex}; 26 | background-image: conic-gradient(${circleEngaged} 0deg, ${circleBg} 0deg); 27 | } 28 | 5% { 29 | background-image: conic-gradient(${circleEngaged} 18deg, ${circleBg} 18deg); 30 | } 31 | 10% { 32 | background-image: conic-gradient(${circleEngaged} 36deg, ${circleBg} 36deg); 33 | } 34 | 15% { 35 | background-image: conic-gradient(${circleEngaged} 54deg, ${circleBg} 54deg); 36 | } 37 | 20% { 38 | background-image: conic-gradient(${circleEngaged} 72deg, ${circleBg} 72deg); 39 | } 40 | 25% { 41 | background-image: conic-gradient(${circleEngaged} 90deg, ${circleBg} 90deg); 42 | } 43 | 30% { 44 | background-image: conic-gradient(${circleEngaged} 108deg, ${circleBg} 108deg); 45 | } 46 | 35% { 47 | background-image: conic-gradient(${circleEngaged} 126deg, ${circleBg} 126deg); 48 | } 49 | 40% { 50 | background-image: conic-gradient(${circleEngaged} 144deg, ${circleBg} 144deg); 51 | } 52 | 45% { 53 | background-image: conic-gradient(${circleEngaged} 162deg, ${circleBg} 162deg); 54 | } 55 | 50% { 56 | background-image: conic-gradient(${circleEngaged} 180deg, ${circleBg} 180deg); 57 | } 58 | 55% { 59 | background-image: conic-gradient(${circleEngaged} 198deg, ${circleBg} 198deg); 60 | } 61 | 60% { 62 | background-image: conic-gradient(${circleEngaged} 216deg, ${circleBg} 216deg); 63 | } 64 | 65% { 65 | background-image: conic-gradient(${circleEngaged} 234deg, ${circleBg} 234deg); 66 | } 67 | 70% { 68 | background-image: conic-gradient(${circleEngaged} 252deg, ${circleBg} 252deg); 69 | } 70 | 75% { 71 | background-image: conic-gradient(${circleEngaged} 270deg, ${circleBg} 270deg); 72 | } 73 | 80% { 74 | background-image: conic-gradient(${circleEngaged} 288deg, ${circleBg} 288deg); 75 | } 76 | 85% { 77 | background-image: conic-gradient(${circleEngaged} 306deg, ${circleBg} 306deg); 78 | } 79 | 90% { 80 | background-image: conic-gradient(${circleEngaged} 324deg, ${circleBg} 324deg); 81 | } 82 | 95% { 83 | background-image: conic-gradient(${circleEngaged} 342deg, ${circleBg} 342deg); 84 | } 85 | 100% { 86 | background-image: conic-gradient(${circleEngaged} 360deg, ${circleBg} 360deg); 87 | } 88 | `}; 89 | `, 90 | [circleEngaged, circleBg, currentQuesIndex], 91 | ); 92 | 93 | return ( 94 | <> 95 | 109 | 123 | 124 | ); 125 | }; 126 | 127 | export default BigQuestionCircle; 128 | -------------------------------------------------------------------------------- /src/modules/game/questioner/bigquestion.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | import { css } from '@emotion/react'; 3 | import { FC, useEffect, useMemo, useRef, useState } from 'react'; 4 | import Ztext from 'react-ztext'; 5 | 6 | import useFitFontSizeToWidth from 'modules/game/questioner/use-fitfontsizetowidth'; 7 | 8 | import BigQuestionCircle from 'modules/game/questioner/bigquestion.circle'; 9 | import { Mode } from 'utils/types'; 10 | import useCensorship from './use-censorship'; 11 | 12 | const BigQuestion: FC<{ 13 | displayQuestion: string; 14 | mode: Mode; 15 | }> = ({ displayQuestion = 'Unknown', mode }) => { 16 | const fontSize = useFitFontSizeToWidth(); 17 | 18 | const censorship = useCensorship(); 19 | const censoredDisplayQuestion = useMemo( 20 | () => censorship(displayQuestion, false), 21 | [displayQuestion, censorship], 22 | ); 23 | 24 | const [size, setSize] = useState(118); 25 | useEffect(() => { 26 | if (window.innerWidth > 320) setSize(140); 27 | if (window.innerWidth > 767) setSize(200); 28 | }, []); 29 | 30 | const spanDom = useRef(null); 31 | useEffect(() => { 32 | if (spanDom.current) { 33 | const nodeList = spanDom.current.querySelectorAll('div > span > span'); 34 | Object.keys(nodeList).forEach((key) => { 35 | nodeList[Number(key)].innerHTML = censoredDisplayQuestion; 36 | }); 37 | 38 | const nodeList2 = spanDom.current.querySelectorAll('div > span'); 39 | Object.keys(nodeList2).forEach((key) => { 40 | nodeList2[Number(key)].classList.remove('bigquestion-animation'); 41 | setTimeout(() => { 42 | nodeList2[Number(key)].classList.add('bigquestion-animation'); 43 | }, 30); 44 | }); 45 | } 46 | }, [censoredDisplayQuestion, censorship]); 47 | 48 | const hiderPositions = ['left', 'top', 'bottom', 'right']; 49 | const hiderPos = useMemo( 50 | () => hiderPositions[Math.floor(Math.random() * hiderPositions.length)], 51 | // eslint-disable-next-line react-hooks/exhaustive-deps 52 | [displayQuestion], 53 | ); 54 | 55 | const isMosaicEnabled = useMemo( 56 | () => 57 | mode === 'veryveryveryhell' && !['1', '2', '3'].includes(displayQuestion), 58 | [mode, displayQuestion], 59 | ); 60 | 61 | const hider = isMosaicEnabled 62 | ? css` 63 | position: relative; 64 | ` 65 | : css``; 66 | 67 | const mosaicWrapper = css` 68 | content: ''; 69 | display: flex; 70 | flex-wrap: wrap; 71 | overflow: hidden; 72 | background: rgba(255, 255, 255, 0.025); 73 | inset: 15%; 74 | ${hiderPos}: 42%; 75 | opacity: 1; 76 | position: absolute; 77 | z-index: 4800; 78 | /* transform: translateZ(6px); */ 79 | transition: 0.2s; 80 | animation: wiggle 0.8s infinite; 81 | 82 | @keyframes wiggle { 83 | 0% { 84 | transform: translate(0px, 0px) rotateZ(0deg); 85 | } 86 | 25% { 87 | transform: translate(4px, 4px) rotateZ(4deg); 88 | } 89 | 50% { 90 | transform: translate(0px, 4px) rotateZ(0deg); 91 | } 92 | 75% { 93 | transform: translate(4px, 0px) rotateZ(-4deg); 94 | } 95 | 100% { 96 | transform: translate(0px, 0px) rotateZ(0deg); 97 | } 98 | } 99 | 100 | & > span { 101 | display: block; 102 | backdrop-filter: blur(60px); 103 | width: 33.33%; 104 | aspect-ratio: 1 / 1; 105 | } 106 | `; 107 | 108 | const bigQuestionContainer = css` 109 | width: ${size}px; 110 | height: ${size}px; 111 | display: inline-flex; 112 | justify-content: center; 113 | align-items: center; 114 | white-space: nowrap; 115 | word-break: keep-all; 116 | font-weight: 900; 117 | color: var(--white); 118 | margin-top: 17px; 119 | position: relative; 120 | ${hider} 121 | `; 122 | 123 | const layersStyle = css` 124 | /*& div > span { 125 | animation: 1.5s linear 0s infinite normal rotate-horizontal; 126 | }*/ 127 | 128 | & div > span > span:not(:first-of-type) { 129 | color: var(--black); 130 | } 131 | `; 132 | 133 | return ( 134 |
135 | {isMosaicEnabled && ( 136 |
137 | {Array(16) 138 | .fill(null) 139 | .map((_, index) => ( 140 | // eslint-disable-next-line react/no-array-index-key 141 | 142 | ))} 143 |
144 | )} 145 | 146 | 147 | 160 | {censoredDisplayQuestion} 161 | 162 | 163 |
164 | ); 165 | }; 166 | 167 | export default BigQuestion; 168 | -------------------------------------------------------------------------------- /src/modules/game/questioner/gamecancelonready.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | import { css } from '@emotion/react'; 3 | import { FC } from 'react'; 4 | import { useSelector } from 'react-redux'; 5 | import { RootState } from 'ducks/rootReducer'; 6 | import { deleteGame, logGameCanceled, pushMessage } from 'utils/database'; 7 | import { localUserNameKey } from 'utils/types'; 8 | import { initialUserName } from 'ducks/user'; 9 | 10 | const GameCancelOnReady: FC = () => { 11 | const gameKey = useSelector((state: RootState) => state.game.key); 12 | const userName = localStorage.getItem(localUserNameKey) || initialUserName; 13 | 14 | const cancel = () => { 15 | pushMessage(gameKey, { 16 | type: 'end', 17 | value: `${userName}によって\nゲームが\n中止されました`, 18 | }); 19 | deleteGame(gameKey); 20 | logGameCanceled(); 21 | }; 22 | 23 | return ( 24 |
30 | 41 |
42 | ); 43 | }; 44 | 45 | export default GameCancelOnReady; 46 | -------------------------------------------------------------------------------- /src/modules/game/questioner/gameready.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | import { css } from '@emotion/react'; 3 | import { FC } from 'react'; 4 | import { modesDisplay, modesCaption, Mode } from 'utils/types'; 5 | 6 | const GameReady: FC<{ 7 | startBy: string; 8 | mode: Mode; 9 | count: number; 10 | }> = ({ startBy, mode, count }) => { 11 | const gameReadyWrapper = css` 12 | animation: 0.4s ease 0s 1 normal blink; 13 | `; 14 | const startHeading = css` 15 | text-align: center; 16 | margin: 0; 17 | color: var(--white); 18 | font-size: 30px; 19 | font-weight: 900; 20 | `; 21 | const modeHeading = css` 22 | text-align: center; 23 | margin: 10px 0 22px; 24 | color: var(--black); 25 | font-size: 30px; 26 | font-weight: 900; 27 | `; 28 | const modeCaption = css` 29 | font-size: 16px; 30 | display: block; 31 | margin-top: 6px; 32 | `; 33 | const usersWrapper = css` 34 | color: var(--black); 35 | margin-bottom: 22px; 36 | `; 37 | 38 | return ( 39 | <> 40 |
41 |

42 | {startBy}が
43 | ゲームを開始! 44 |

45 |

46 | {modesDisplay[mode]} 47 | {modesCaption[mode]} 48 |

49 |
50 |
51 | {count}人の参加者 52 |
53 | 54 | ); 55 | }; 56 | 57 | export default GameReady; 58 | -------------------------------------------------------------------------------- /src/modules/game/questioner/questioner.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | import { css } from '@emotion/react'; 3 | import { FC } from 'react'; 4 | import { useSelector } from 'react-redux'; 5 | import { RootState } from 'ducks/rootReducer'; 6 | import { FinishGameFunction, modesConvert, modesDetail } from 'utils/types'; 7 | import useQuestionTimer from 'modules/game/questioner/use-questiontimer'; 8 | 9 | import BigQuestion from 'modules/game/questioner/bigquestion'; 10 | import QuestionList from 'modules/game/questioner/questionlistcontainer'; 11 | import AnswerDisplay from 'modules/game/questioner/answerdisplay'; 12 | import GameReady from 'modules/game/questioner/gameready'; 13 | import GameCancelOnReady from 'modules/game/questioner/gamecancelonready'; 14 | 15 | const Questioner: FC<{ 16 | finishGame: FinishGameFunction; 17 | }> = ({ finishGame }) => { 18 | const gameObj = useSelector((state: RootState) => state.game.entity); 19 | const isDuringGame = useSelector( 20 | (state: RootState) => state.game.isDuringGame, 21 | ); 22 | const currentQuesIndex = useSelector( 23 | (state: RootState) => state.game.currentQuesIndex, 24 | ); 25 | 26 | useQuestionTimer(finishGame); 27 | 28 | const displayQuestions = 29 | gameObj && 30 | gameObj.questions.map((question) => 31 | isDuringGame 32 | ? modesConvert[gameObj.mode](question) 33 | : modesConvert.easy(question), 34 | ); 35 | 36 | const questionerContainer = css` 37 | grid-area: questioner; 38 | width: 100%; 39 | padding-right: 10px; 40 | display: flex; 41 | flex-direction: column; 42 | align-items: center; 43 | justify-content: center; 44 | `; 45 | const modeDisp = css` 46 | font-size: 15px; 47 | font-weight: 700; 48 | text-align: center; 49 | color: var(--black); 50 | `; 51 | 52 | return ( 53 |
54 | {isDuringGame && ( 55 |
56 | {!!gameObj && modesDetail[gameObj.mode]} 57 |
58 | )} 59 | 60 | {!!gameObj && ( 61 | 66 | )} 67 | 68 | {isDuringGame && !!displayQuestions && ( 69 | 2 73 | ? displayQuestions[currentQuesIndex - 1] 74 | : gameObj.questions[currentQuesIndex - 1] 75 | } 76 | /> 77 | )} 78 | 79 | {!!gameObj && } 80 | 81 | {!isDuringGame && !!gameObj?.answer && ( 82 | 83 | )} 84 | 85 | {!!displayQuestions && 86 | gameObj.mode !== 'veryveryhell' && 87 | gameObj.mode !== 'veryveryveryhell' && ( 88 | 92 | )} 93 | {!!displayQuestions && 94 | !isDuringGame && 95 | (gameObj.mode === 'veryveryhell' || 96 | gameObj.mode === 'veryveryveryhell') && ( 97 | 101 | )} 102 |
103 | ); 104 | }; 105 | 106 | export default Questioner; 107 | -------------------------------------------------------------------------------- /src/modules/game/questioner/questionlist.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | import { css } from '@emotion/react'; 3 | import { FC } from 'react'; 4 | 5 | import { useSelector } from 'react-redux'; 6 | import { RootState } from 'ducks/rootReducer'; 7 | 8 | import { useGenerateColor } from 'hooks/use-generates'; 9 | import useCensorship from './use-censorship'; 10 | 11 | const QuestionList: FC<{ 12 | questions: string[]; 13 | current: number; 14 | }> = ({ questions, current }) => { 15 | const isDuringGame = useSelector( 16 | (state: RootState) => state.game.isDuringGame, 17 | ); 18 | const generateColor = useGenerateColor(); 19 | 20 | const listItem = isDuringGame 21 | ? css` 22 | margin-right: 3px; 23 | margin-bottom: 3px; 24 | font-size: 15px; 25 | letter-spacing: -1px; 26 | transition: 0.4s ease; 27 | line-height: 1; 28 | height: 1em; 29 | animation: 0.4s ease 0s 1 normal dynamicclicked; 30 | color: var(--black); 31 | ` 32 | : css` 33 | padding: 1px 5px; 34 | border-radius: 15px; 35 | margin-right: 3px; 36 | margin-bottom: 5px; 37 | font-size: 13px; 38 | letter-spacing: -1px; 39 | animation: 0.4s ease 0s 1 both dynamicclicked; 40 | color: var(--primary-color); 41 | `; 42 | const listHiddenItem = css` 43 | width: 2px; 44 | height: 1em; 45 | background: var(--red); 46 | opacity: 0.7; 47 | display: block; 48 | line-height: 1; 49 | `; 50 | 51 | const censorship = useCensorship(); 52 | 53 | return ( 54 | <> 55 | {questions 56 | .map((question, index) => 57 | index < current || !isDuringGame ? ( 58 | 71 | {isDuringGame 72 | ? censorship(question, false) 73 | : censorship(question, true)} 74 | 75 | ) : ( 76 | 81 | ), 82 | ) 83 | .filter((q, index) => index > 2)} 84 | 85 | ); 86 | }; 87 | 88 | export default QuestionList; 89 | -------------------------------------------------------------------------------- /src/modules/game/questioner/questionlistcontainer.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | import { css } from '@emotion/react'; 3 | import { FC, useEffect, useRef } from 'react'; 4 | 5 | import { useSelector } from 'react-redux'; 6 | import { RootState } from 'ducks/rootReducer'; 7 | 8 | import QuestionList from 'modules/game/questioner/questionlist'; 9 | 10 | const QuestionListContainer: FC<{ 11 | questions: string[]; 12 | // eslint-disable-next-line react/require-default-props 13 | current?: number; 14 | }> = ({ questions, current = 0 }) => { 15 | const isDuringGame = useSelector( 16 | (state: RootState) => state.game.isDuringGame, 17 | ); 18 | 19 | const listDom = useRef(null); 20 | useEffect(() => { 21 | if (listDom.current) { 22 | let top = listDom.current.scrollHeight; 23 | if (!isDuringGame) top = 0; 24 | listDom.current.scrollBy({ 25 | top, 26 | behavior: 'smooth', 27 | }); 28 | } 29 | }, [current, isDuringGame]); 30 | 31 | return ( 32 |
59 | 60 | {/* isDuringGame && ( 61 | 67 | ({30 - current}) 68 | 69 | ) */} 70 |
71 | ); 72 | }; 73 | 74 | export default QuestionListContainer; 75 | -------------------------------------------------------------------------------- /src/modules/game/questioner/use-censorship.ts: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | 3 | const useCensorship = () => 4 | useCallback((question: string, show: boolean): string => { 5 | const pattern = /\{\{([^}]+)\}\}/g; 6 | 7 | if (!show) 8 | return question.replace(pattern, (match, p1: string) => 9 | '█'.repeat(p1.length), 10 | ); 11 | 12 | return question.replace(pattern, (match, p1: string) => p1); 13 | }, []); 14 | 15 | export default useCensorship; 16 | -------------------------------------------------------------------------------- /src/modules/game/questioner/use-fitfontsizetowidth.ts: -------------------------------------------------------------------------------- 1 | type FontSizeVw = `${string}vw`; 2 | 3 | const useFitFontSizeToWidth = 4 | () => 5 | (width: number, text: string, letterSpacing = 1): FontSizeVw => { 6 | const wrapper = width / window.innerWidth; 7 | const textLength = text.length > 2 ? text.length : 2; 8 | const fontSizeVw = (wrapper / (textLength * letterSpacing)) * 100; 9 | 10 | return `${fontSizeVw.toString()}vw`; 11 | }; 12 | 13 | export default useFitFontSizeToWidth; 14 | -------------------------------------------------------------------------------- /src/modules/game/questioner/use-questiontimer.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useRef } from 'react'; 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | import { RootState } from 'ducks/rootReducer'; 4 | import { gameSlice } from 'ducks/game'; 5 | import { getSummary, updateSummaryFromKey } from 'utils/summary'; 6 | import { pushMessage } from 'utils/database'; 7 | import { FinishGameFunction, gameTimerSeconds } from 'utils/types'; 8 | import useAudio from 'hooks/use-audio'; 9 | 10 | const useQuestionTimer = (finishGame: FinishGameFunction) => { 11 | const dispatch = useDispatch(); 12 | const { proceedQuesIndex } = gameSlice.actions; 13 | const gameObj = useSelector((state: RootState) => state.game.entity); 14 | const gameKey = useSelector((state: RootState) => state.game.key); 15 | const messages = useSelector((state: RootState) => state.game.messages); 16 | const isDuringGame = useSelector( 17 | (state: RootState) => state.game.isDuringGame, 18 | ); 19 | const currentQuesIndex = useSelector( 20 | (state: RootState) => state.game.currentQuesIndex, 21 | ); 22 | 23 | const timerId = useRef(); 24 | const clearTimer = useCallback(() => clearInterval(timerId.current), []); 25 | 26 | const playSE = useAudio(); 27 | 28 | useEffect(() => { 29 | if (isDuringGame) { 30 | timerId.current = setInterval(() => { 31 | dispatch(proceedQuesIndex()); 32 | playSE('question'); 33 | }, gameTimerSeconds * 1000); 34 | } 35 | 36 | return clearTimer; 37 | }, [isDuringGame, dispatch, proceedQuesIndex, clearTimer, playSE]); 38 | 39 | useEffect(() => { 40 | if (currentQuesIndex === 2) { 41 | const summary = getSummary(); 42 | if (summary && summary.lastPlay !== gameObj?.created) { 43 | updateSummaryFromKey('playCount', (count) => count + 1); 44 | updateSummaryFromKey('lastPlay', gameObj?.created ?? Date.now()); 45 | } 46 | } 47 | 48 | if ( 49 | gameObj && 50 | currentQuesIndex === gameObj.questions.length && 51 | gameObj.questions.length !== 0 && 52 | messages.find((msg) => msg.type === 'end') === undefined 53 | ) { 54 | pushMessage(gameKey, { 55 | type: 'end', 56 | value: '誰も答えられませんでした', 57 | }); 58 | finishGame(); 59 | clearTimer(); 60 | } 61 | }, [ 62 | currentQuesIndex, 63 | gameObj, 64 | gameKey, 65 | messages, 66 | finishGame, 67 | clearTimer, 68 | playSE, 69 | ]); 70 | 71 | return clearTimer; 72 | }; 73 | 74 | export default useQuestionTimer; 75 | -------------------------------------------------------------------------------- /src/modules/game/use-finishgame.ts: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | import { deleteGame } from 'utils/database'; 3 | 4 | import { useDispatch, useSelector } from 'react-redux'; 5 | import { RootState } from 'ducks/rootReducer'; 6 | import { gameSlice } from 'ducks/game'; 7 | import { FinishGameFunction } from 'utils/types'; 8 | 9 | const useFinishGame = (): FinishGameFunction => { 10 | const gameKey = useSelector((state: RootState) => state.game.key); 11 | const dispatch = useDispatch(); 12 | const { stopGame, unsetGame } = gameSlice.actions; 13 | 14 | return useCallback( 15 | (isDeleted = false, isUnset = false) => { 16 | if (!isDeleted) deleteGame(gameKey); 17 | 18 | dispatch(stopGame()); 19 | 20 | if (isUnset) dispatch(unsetGame()); 21 | document.body.classList.remove('ready'); 22 | 23 | document.body.style.backgroundColor = 'var(--bg-color)'; 24 | document 25 | .querySelector("meta[name='theme-color']") 26 | ?.setAttribute('content', '#f2efe2'); 27 | document 28 | .querySelector("meta[name='theme-color'][media*='dark']") 29 | ?.setAttribute('content', '#0b141c'); 30 | }, 31 | [gameKey, dispatch, stopGame, unsetGame], 32 | ); 33 | }; 34 | 35 | export default useFinishGame; 36 | -------------------------------------------------------------------------------- /src/modules/game/use-fitScreenHeight.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | const useFitScreenHeight = (): number => { 4 | const [gameHeight, setGameHeight] = useState(visualViewport?.height ?? 0); 5 | 6 | const onWindowResize = () => { 7 | window.scrollTo(0, 0); 8 | setGameHeight(visualViewport?.height ?? 0); 9 | }; 10 | useEffect(() => { 11 | window.visualViewport?.addEventListener('resize', onWindowResize); 12 | 13 | return () => { 14 | window.visualViewport?.removeEventListener('resize', onWindowResize); 15 | }; 16 | }, []); 17 | 18 | return gameHeight; 19 | }; 20 | 21 | export default useFitScreenHeight; 22 | -------------------------------------------------------------------------------- /src/modules/game/use-gamestarted.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { listenGameDeleted, logPageView } from 'utils/database'; 3 | import { getSummary, updateSummaryFromKey } from 'utils/summary'; 4 | 5 | import { useSelector } from 'react-redux'; 6 | import { RootState } from 'ducks/rootReducer'; 7 | import useFinishGame from 'modules/game/use-finishgame'; 8 | import { gameTimerSeconds } from 'utils/types'; 9 | import useAudio from 'hooks/use-audio'; 10 | 11 | const useGameStarted = () => { 12 | const gameKey = useSelector((state: RootState) => state.game.key); 13 | const gameObj = useSelector((state: RootState) => state.game.entity); 14 | 15 | const isDuringGame = useSelector( 16 | (state: RootState) => state.game.isDuringGame, 17 | ); 18 | 19 | const finishGame = useFinishGame(); 20 | 21 | const playSE = useAudio(); 22 | 23 | useEffect(() => { 24 | listenGameDeleted(gameKey, () => { 25 | const summary = getSummary(); 26 | if (summary && summary.lastPlay !== summary.lastWon) { 27 | updateSummaryFromKey('currentStreak', 0); 28 | } 29 | finishGame(true); 30 | }); 31 | 32 | if (isDuringGame) { 33 | logPageView('game'); 34 | document.body.classList.add('ready'); 35 | document.body.style.backgroundColor = gameObj?.color ?? 'var(--bg-color)'; 36 | document 37 | .querySelector("meta[name='theme-color']") 38 | ?.setAttribute('content', gameObj?.color ?? '#f2efe2'); 39 | setTimeout(() => { 40 | document.body.classList.remove('ready'); 41 | }, gameTimerSeconds * 3 * 1000); 42 | } else { 43 | logPageView('game_end'); 44 | } 45 | 46 | return () => { 47 | if (!!gameObj?.created && Date.now() - gameObj.created < 1000) { 48 | playSE('cancel'); 49 | finishGame(false, true); 50 | alert('エラー:複数のゲームが同時に開始されました。やり直してください'); 51 | } else { 52 | finishGame(); 53 | } 54 | }; 55 | // eslint-disable-next-line react-hooks/exhaustive-deps 56 | }, [gameKey, gameObj, finishGame, isDuringGame]); 57 | 58 | return finishGame; 59 | }; 60 | 61 | export default useGameStarted; 62 | -------------------------------------------------------------------------------- /src/modules/home/gamesetter.modeselector.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | import { css } from '@emotion/react'; 3 | import { FaCaretDown, FaCaretUp } from 'react-icons/fa'; 4 | import { Mode, modesCaption, modesDisplay } from 'utils/types'; 5 | import { FC, useEffect, useMemo, useRef, useState } from 'react'; 6 | import useAudio from 'hooks/use-audio'; 7 | 8 | const ModeButton = ({ 9 | targetMode, 10 | currentMode, 11 | setMode, 12 | }: { 13 | targetMode: Mode; 14 | currentMode: Mode; 15 | setMode: (mode: Mode) => void; 16 | }) => { 17 | const buttonRef = useRef(null); 18 | useEffect(() => { 19 | if ( 20 | buttonRef.current && 21 | targetMode === currentMode && 22 | currentMode !== 'easy' 23 | ) { 24 | buttonRef.current.scrollIntoView({ 25 | behavior: 'smooth', 26 | block: 'center', 27 | inline: 'center', 28 | }); 29 | } 30 | }, [currentMode, targetMode]); 31 | 32 | const playSE = useAudio(); 33 | 34 | return ( 35 | 47 | ); 48 | }; 49 | 50 | const ModeSelector: FC<{ 51 | mode: Mode; 52 | setMode: (mode: Mode) => void; 53 | }> = ({ mode, setMode }) => { 54 | const modeCaption = useMemo(() => modesCaption[mode], [mode]); 55 | 56 | const [isCollapse, setIsCollapse] = useState( 57 | Object.keys(modesDisplay).indexOf(mode) >= 3, 58 | ); 59 | 60 | const moreRef = useRef(null); 61 | useEffect(() => { 62 | const onWheel = (e: WheelEvent) => { 63 | if (Math.abs(e.deltaY) < Math.abs(e.deltaX)) return; 64 | e.preventDefault(); 65 | if (moreRef.current) moreRef.current.scrollLeft += e.deltaY; 66 | }; 67 | 68 | if (moreRef.current) moreRef.current.addEventListener('wheel', onWheel); 69 | 70 | return () => { 71 | if (moreRef.current) 72 | // eslint-disable-next-line react-hooks/exhaustive-deps 73 | moreRef.current.removeEventListener('wheel', onWheel); 74 | }; 75 | }, []); 76 | 77 | const modeCaptionStyle = css` 78 | font-weight: 700; 79 | font-size: 16px; 80 | text-align: center; 81 | color: var(--mode-caption-color); 82 | `; 83 | 84 | const selectorContainer = css` 85 | margin: 0 auto; 86 | width: 340px; 87 | max-width: 95vw; 88 | max-width: calc(100vw - 60px); 89 | position: relative; 90 | 91 | &:after, 92 | &:before { 93 | content: ''; 94 | width: 10px; 95 | height: 60px; 96 | background-image: linear-gradient( 97 | 90deg, 98 | transparent 0%, 99 | var(--bg-color) 100% 100 | ); 101 | display: block; 102 | position: absolute; 103 | right: 0; 104 | pointer-events: none; 105 | top: 0; 106 | } 107 | 108 | &:before { 109 | background-image: linear-gradient( 110 | -90deg, 111 | transparent 0%, 112 | var(--bg-color) 100% 113 | ); 114 | left: 0; 115 | } 116 | 117 | @media (min-width: 768px) { 118 | width: auto; 119 | 120 | &:before, 121 | &:after { 122 | content: none; 123 | } 124 | } 125 | `; 126 | const selectorInner = css` 127 | display: flex; 128 | overflow-y: auto; 129 | padding-left: 10px; 130 | scrollbar-width: none; 131 | align-items: center; 132 | 133 | &::-webkit-scrollbar { 134 | display: none; 135 | } 136 | 137 | &:after { 138 | content: ''; 139 | display: block; 140 | min-width: 80px; 141 | } 142 | 143 | @media (min-width: 768px) { 144 | padding-left: 0; 145 | flex-wrap: wrap; 146 | align-items: center; 147 | justify-content: center; 148 | overflow: visible; 149 | 150 | &:after { 151 | content: none; 152 | } 153 | } 154 | `; 155 | 156 | const playSE = useAudio(); 157 | 158 | return ( 159 | <> 160 |
161 |
162 | {Object.keys(modesDisplay).map((key, index) => { 163 | if (index < 4) 164 | return ( 165 | 171 | ); 172 | if (index === 4) 173 | return ( 174 | 195 | ); 196 | 197 | return true; 198 | })} 199 | 200 |
201 |
202 | {Object.keys(modesDisplay).map((key, index) => { 203 | if (index >= 4) 204 | return ( 205 | 211 | ); 212 | 213 | return true; 214 | })} 215 |
216 |
217 |
218 |
219 |

{modeCaption}

220 | 221 | ); 222 | }; 223 | 224 | export default ModeSelector; 225 | -------------------------------------------------------------------------------- /src/modules/home/gamesetter.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useState } from 'react'; 2 | 3 | import { localUserNameKey, Mode, Users } from 'utils/types'; 4 | import { initialUserName } from 'ducks/user'; 5 | import usePushGame from 'modules/home/use-pushgame'; 6 | import ModeSelector from 'modules/home/gamesetter.modeselector'; 7 | 8 | const GameSetter: FC<{ 9 | users: Users | undefined; 10 | lastMode: Mode; 11 | }> = ({ users, lastMode }) => { 12 | const userName = localStorage.getItem(localUserNameKey) || initialUserName; 13 | 14 | const [mode, setMode] = useState(lastMode); 15 | 16 | const pushGame = usePushGame(); 17 | 18 | return ( 19 | <> 20 | {users && ( 21 | <> 22 | setMode(newMode)} /> 23 | 24 | 31 | 32 | )} 33 | 34 | {!users && } 35 | 36 | ); 37 | }; 38 | 39 | export default GameSetter; 40 | -------------------------------------------------------------------------------- /src/modules/home/home.logo.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | import { css } from '@emotion/react'; 3 | import { FC } from 'react'; 4 | import { TbInfoCircle } from 'react-icons/tb'; 5 | 6 | const HomeLogo: FC = () => { 7 | const homeLogoContainer = css` 8 | grid-area: logo; 9 | display: flex; 10 | align-items: center; 11 | flex-direction: column; 12 | justify-content: end; 13 | padding: 30px 0; 14 | `; 15 | const homeLogo = css` 16 | width: 90px; 17 | margin: 0; 18 | 19 | @media (min-width: 768px) { 20 | width: 120px; 21 | } 22 | `; 23 | const homeLogoCopy = css` 24 | font-size: 17px; 25 | font-weight: 900; 26 | line-height: 1; 27 | margin: 8px 0 15px; 28 | text-align: center; 29 | line-height: 1.35; 30 | `; 31 | 32 | return ( 33 |
34 |

35 | 40 | 41 | 45 | 49 | 53 | 54 |

55 |

56 | 市町村 57 | ul { 63 | z-index: 5025; 64 | display: inline-block; 65 | } 66 | `} 67 | > 68 |
    119 |
  • 特別区(東京23区)を含む
  • 120 |
  • 市町村以外のモードも数多く用意しています
  • 121 |
122 | 127 |
128 | から 129 |
130 | 138 | 都道府県を当てる 139 | 140 | だけ! 141 |

142 |
143 | ); 144 | }; 145 | 146 | export default HomeLogo; 147 | -------------------------------------------------------------------------------- /src/modules/home/home.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | import { css } from '@emotion/react'; 3 | import { FC } from 'react'; 4 | 5 | import useUserMounted from 'modules/home/use-usermounted'; 6 | 7 | import OnlineUsers from 'modules/home/onlineusers'; 8 | import GameSetter from 'modules/home/gamesetter'; 9 | import HomeLogo from 'modules/home/home.logo'; 10 | import bg from 'assets/svg/home-bg.svg'; 11 | import { Mode } from 'utils/types'; 12 | 13 | const Home: FC<{ editMode: () => void; lastMode: Mode }> = ({ 14 | editMode, 15 | lastMode, 16 | }) => { 17 | const users = useUserMounted(); 18 | 19 | const homeContainer = css` 20 | display: grid; 21 | grid-template-columns: 1fr; 22 | grid-template-areas: 23 | 'bg' 24 | 'logo' 25 | 'users' 26 | 'setter'; 27 | min-height: 100vh; 28 | width: 100vw; 29 | 30 | @media (min-width: 768px) { 31 | grid-template-columns: 1fr 280px 3fr; 32 | grid-template-areas: 33 | 'bg logo users' 34 | 'bg setter users'; 35 | } 36 | `; 37 | const homeLeftBg = css` 38 | display: none; 39 | grid-area: bg; 40 | background-image: url(${bg}); 41 | background-repeat: repeat-x; 42 | background-size: cover; 43 | background-position: right center; 44 | 45 | @media (min-width: 768px) { 46 | display: block; 47 | } 48 | `; 49 | const homeOnlineUsersContainer = css` 50 | grid-area: users; 51 | display: flex; 52 | align-items: center; 53 | justify-content: center; 54 | 55 | @media (min-width: 768px) { 56 | background-image: url(${bg}); 57 | background-repeat: repeat-x; 58 | background-position: left center; 59 | background-size: cover; 60 | justify-content: start; 61 | padding: 0 30px 0 10vw; 62 | } 63 | `; 64 | const homeGameSetterContainer = css` 65 | grid-area: setter; 66 | padding-top: 15px; 67 | 68 | @media (min-width: 768px) { 69 | padding-top: 20px; 70 | } 71 | `; 72 | 73 | return ( 74 |
75 |
76 | 77 |
78 | editMode()} /> 79 |
80 |
81 | 82 |
83 |
84 | ); 85 | }; 86 | 87 | export default Home; 88 | -------------------------------------------------------------------------------- /src/modules/home/onlineusers.header.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | import { css } from '@emotion/react'; 3 | import useAudio from 'hooks/use-audio'; 4 | import { FC, useEffect, useRef, useState } from 'react'; 5 | 6 | const OnlineUsersHeader: FC<{ 7 | usersLength: number; 8 | editMode: () => void; 9 | }> = ({ usersLength, editMode }) => { 10 | const playSE = useAudio(); 11 | 12 | const [showReload, setShowReload] = useState(false); 13 | const timerId = useRef(); 14 | const clearTimer = () => clearTimeout(timerId.current); 15 | 16 | useEffect(() => { 17 | timerId.current = setTimeout(() => { 18 | setShowReload(true); 19 | }, 2000); 20 | 21 | return () => { 22 | setShowReload(false); 23 | clearTimer(); 24 | }; 25 | }, []); 26 | 27 | return ( 28 |
43 | {usersLength === 0 ? ( 44 | <> 45 | 接続中… 46 | {showReload && ( 47 | 56 | )} 57 | 58 | ) : ( 59 | <> 60 | 65 | 現在、 66 | 67 | {usersLength}人 68 | 69 | が接続中 70 | 71 | 81 | 82 | )} 83 |
84 | ); 85 | }; 86 | 87 | export default OnlineUsersHeader; 88 | -------------------------------------------------------------------------------- /src/modules/home/onlineusers.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | import { css } from '@emotion/react'; 3 | import { Users } from 'utils/types'; 4 | import { FC, useEffect, useState } from 'react'; 5 | 6 | import backgroundImage from 'assets/svg/sp-bg.svg'; 7 | import OnlineUsersHeader from 'modules/home/onlineusers.header'; 8 | import UserList from 'modules/home/onlineusers.userlist'; 9 | 10 | const OnlineUsers: FC<{ 11 | users: Users | undefined; 12 | editMode: () => void; 13 | }> = ({ users, editMode }) => { 14 | const pingThreshold = 7700; 15 | 16 | const [usersLength, setUsersLength] = useState(0); 17 | useEffect(() => { 18 | setUsersLength( 19 | users 20 | ? Object.keys(users).filter( 21 | (key) => Date.now() - users[key].pingStamp < pingThreshold, 22 | ).length 23 | : 0, 24 | ); 25 | }, [users]); 26 | 27 | const onlineUsersContainer = css` 28 | border: 4px solid var(--primary-color); 29 | border-radius: 20px; 30 | padding: 15px 20px; 31 | backdrop-filter: blur(30px); 32 | width: 340px; 33 | max-width: 95%; 34 | max-width: calc(100vw - 60px); 35 | transition: 0.2s; 36 | background-image: url(${backgroundImage}); 37 | background-size: 180%; 38 | background-position: -50px -50px; 39 | 40 | @media (min-width: 768px) { 41 | width: auto; 42 | min-width: 350px; 43 | } 44 | `; 45 | 46 | return ( 47 |
48 | 49 | 54 |
55 | ); 56 | }; 57 | 58 | export default OnlineUsers; 59 | -------------------------------------------------------------------------------- /src/modules/home/onlineusers.userlist.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | import { css } from '@emotion/react'; 3 | import { Users } from 'utils/types'; 4 | import { FC } from 'react'; 5 | 6 | import { useSelector } from 'react-redux'; 7 | import { RootState } from 'ducks/rootReducer'; 8 | 9 | import DeviceIcon from 'components/atoms/deviceicon'; 10 | 11 | const UserList: FC<{ 12 | users: Users | undefined; 13 | usersLength: number; 14 | pingThreshold: number; 15 | }> = ({ users, usersLength, pingThreshold }) => { 16 | const me = useSelector((state: RootState) => state.user.key); 17 | 18 | const userList = css` 19 | margin: 0; 20 | padding: 10px 17px 7px; 21 | min-height: 50px; 22 | max-height: 200px; 23 | list-style-type: none; 24 | overflow-y: auto; 25 | display: flex; 26 | flex-direction: column; 27 | justify-content: center; 28 | 29 | @media (min-width: 768px) { 30 | padding: 25px 23px 21px; 31 | min-height: 110px; 32 | max-height: 270px; 33 | } 34 | 35 | @media (max-width: 400px) { 36 | padding: 10px 8px 7px; 37 | } 38 | `; 39 | const userListItem = (color: string) => css` 40 | margin: 4px 0; 41 | --user-color: ${/^[#A-Za-z0-9]+$/.test(color) ? color : 'var(--red)'}; 42 | display: flex; 43 | flex-direction: row; 44 | align-items: center; 45 | font-weight: 700; 46 | line-height: 1; 47 | flex-wrap: wrap; 48 | animation: 0.2s ease 0s 1 normal clicked; 49 | 50 | @media (min-width: 768px) { 51 | margin: 7px 0; 52 | } 53 | `; 54 | const userScore = css` 55 | margin: 0 7px; 56 | font-size: 13px; 57 | font-weight: 600; 58 | opacity: 0.6; 59 | 60 | @media (max-width: 400px) { 61 | margin-right: 0; 62 | } 63 | `; 64 | const myDevice = css` 65 | font-size: 13px; 66 | font-weight: 600; 67 | color: var(--red); 68 | margin: 3px 0; 69 | 70 | @media (max-width: 400px) { 71 | width: 130px; 72 | padding-left: 23px; 73 | margin-top: 3px; 74 | margin-bottom: 4px; 75 | &:before { 76 | content: '('; 77 | } 78 | &:after { 79 | content: ')'; 80 | } 81 | } 82 | `; 83 | 84 | const reload = css` 85 | ${userListItem('var(--red)')} 86 | margin-top: 13px !important; 87 | font-size: 14px; 88 | 89 | & button { 90 | font-size: 16px; 91 | text-decoration: underline; 92 | text-underline-offset: 4px; 93 | } 94 | `; 95 | 96 | return ( 97 |
    98 | {users && 99 | Object.keys(users).map( 100 | (key) => 101 | Date.now() - users[key].pingStamp < pingThreshold && ( 102 |
  • 103 | 104 | {users[key].userName} 105 | スコア{users[key].score || '0'} 106 | {key === me && この端末} 107 |
  • 108 | ), 109 | )} 110 | {users && usersLength > 0 && !Object.keys(users).includes(me) && ( 111 |
  • 112 | この端末が表示されていない場合は 113 | 122 |
  • 123 | )} 124 | {usersLength === 0 && } 125 |
126 | ); 127 | }; 128 | 129 | export default UserList; 130 | -------------------------------------------------------------------------------- /src/modules/home/use-pushgame.ts: -------------------------------------------------------------------------------- 1 | import shuffle from 'lodash/shuffle'; 2 | import { prefecture } from 'assets/data/prefecture'; 3 | import { logStartGame, writeNewGame } from 'utils/database'; 4 | import { 5 | Mode, 6 | PrefectureStr, 7 | Questions, 8 | modesCaption, 9 | modesConvert, 10 | } from 'utils/types'; 11 | 12 | const colors = [ 13 | '#51B1C9', 14 | '#6DCE97', 15 | '#76B2B4', 16 | '#8FB505', 17 | '#9F68E8', 18 | '#C9C21E', 19 | '#D6A671', 20 | '#E8A21C', 21 | '#ED9489', 22 | '#F1B8B5', 23 | '#FFB554', 24 | ]; 25 | 26 | const randomMode = (): Mode => { 27 | const filteredModes = Object.keys(modesCaption).filter( 28 | (mode) => mode !== 'random', 29 | ); 30 | const randomIndex = Math.floor(Math.random() * filteredModes.length); 31 | 32 | return filteredModes[randomIndex] as Mode; 33 | }; 34 | 35 | type ImportData = typeof import('assets/data/cities'); 36 | 37 | const usePushGame = 38 | () => (mode: Mode, startBy: string, gameUsers: string[]) => { 39 | const randomPref: PrefectureStr = 40 | prefecture[Math.floor(Math.random() * prefecture.length)]; 41 | 42 | const modeUltimate = mode === 'random' ? randomMode() : mode; 43 | 44 | const write = (questions: Questions) => 45 | writeNewGame({ 46 | answer: randomPref, 47 | questions, 48 | mode: modeUltimate, 49 | startBy, 50 | messages: [], 51 | color: colors[Math.floor(Math.random() * colors.length)], 52 | users: gameUsers, 53 | created: Date.now(), 54 | }); 55 | 56 | const countDown: ('3' | '2' | '1')[] = ['3', '2', '1']; 57 | const mixedCityModes: Mode[] = ['easy', 'normal', 'veryveryhell']; 58 | 59 | const importPaths = [ 60 | 'stations', 61 | 'mountains', 62 | 'cities', 63 | 'castles', 64 | 'reststops', 65 | 'goods', 66 | 'specialties', 67 | 'cuisines', 68 | 'attractions', 69 | 'sweets', 70 | 'museums', 71 | 'spas', 72 | 'festivals', 73 | 'powerplants', 74 | 'quiz', 75 | ] as const; 76 | type ImportPath = typeof importPaths[number]; 77 | const mappingPaths: { [key in Mode]?: ImportPath } = { 78 | station: 'stations', 79 | mountain: 'mountains', 80 | castle: 'castles', 81 | reststop: 'reststops', 82 | goods: 'goods', 83 | specialty: 'specialties', 84 | cuisine: 'cuisines', 85 | attraction: 'attractions', 86 | sweets: 'sweets', 87 | museum: 'museums', 88 | spa: 'spas', 89 | festival: 'festivals', 90 | powerplant: 'powerplants', 91 | quiz: 'quiz', 92 | }; 93 | const importPath: ImportPath = mappingPaths[modeUltimate] || 'cities'; 94 | 95 | import(`assets/data/${importPath}`) 96 | .then(async (data: ImportData) => { 97 | if (modeUltimate === 'mixed') { 98 | let importedData: string[] = []; 99 | await Promise.all( 100 | importPaths.map(async (path) => { 101 | importedData = [ 102 | ...importedData, 103 | ...( 104 | (await import(`assets/data/${path}`)) as ImportData 105 | ).default()[randomPref], 106 | ]; 107 | }), 108 | ); 109 | const cities = data 110 | .default() 111 | [randomPref].map((city) => 112 | modesConvert[ 113 | mixedCityModes[ 114 | Math.floor(Math.random() * mixedCityModes.length) 115 | ] 116 | ](city), 117 | ); 118 | write([ 119 | ...countDown, 120 | ...shuffle([...cities, ...importedData]).slice(0, 30), 121 | ]); 122 | } else { 123 | write([ 124 | ...countDown, 125 | ...shuffle(data.default()[randomPref]).slice(0, 30), 126 | ]); 127 | } 128 | logStartGame(randomPref, mode, gameUsers.length, startBy); 129 | }) 130 | .catch((err) => { 131 | alert('データを読み込めませんでした'); 132 | console.error(err); 133 | }); 134 | }; 135 | 136 | export default usePushGame; 137 | -------------------------------------------------------------------------------- /src/modules/home/use-usermounted.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from 'react'; 2 | import { useSelector } from 'react-redux'; 3 | import { RootState } from 'ducks/rootReducer'; 4 | import { Users } from 'utils/types'; 5 | import { listenUsers, updatePingStamp } from 'utils/database'; 6 | import useAudio from 'hooks/use-audio'; 7 | 8 | const useUserMounted = () => { 9 | const userKey = useSelector((state: RootState) => state.user.key); 10 | 11 | const [users, setUsers] = useState(); 12 | 13 | const timerId = useRef(); 14 | const clearTimer = () => clearInterval(timerId.current); 15 | 16 | const playSE = useAudio(); 17 | 18 | useEffect(() => { 19 | listenUsers((data) => { 20 | setUsers((prevUsers) => { 21 | const prevKeys = Object.keys(prevUsers ?? {}); 22 | const currentKeys = Object.keys(data ?? {}).filter( 23 | (key) => Date.now() - data[key].pingStamp < 7700, 24 | ); 25 | if (JSON.stringify(prevKeys) !== JSON.stringify(currentKeys)) { 26 | if (prevKeys < currentKeys) playSE('online'); 27 | if (prevKeys > currentKeys) playSE('offline'); 28 | } 29 | 30 | return currentKeys.reduce((result: Users, ObjectKey: string) => { 31 | // eslint-disable-next-line no-param-reassign 32 | result[ObjectKey] = data[ObjectKey]; 33 | 34 | return result; 35 | }, {}); 36 | }); 37 | }); 38 | 39 | document.body.style.backgroundColor = 'var(--bg-color)'; 40 | // eslint-disable-next-line react-hooks/exhaustive-deps 41 | }, [userKey]); 42 | 43 | useEffect(() => { 44 | timerId.current = setInterval(() => { 45 | if (userKey && users && Object.keys(users).includes(userKey)) { 46 | updatePingStamp(userKey); 47 | } 48 | }, 3000); 49 | 50 | return clearTimer; 51 | }, [userKey, users]); 52 | 53 | return users; 54 | }; 55 | 56 | export default useUserMounted; 57 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals') 6 | .then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 7 | getCLS(onPerfEntry); 8 | getFID(onPerfEntry); 9 | getFCP(onPerfEntry); 10 | getLCP(onPerfEntry); 11 | getTTFB(onPerfEntry); 12 | }) 13 | .catch((err) => console.log(err)); // eslint-disable-line no-console 14 | } 15 | }; 16 | 17 | export default reportWebVitals; 18 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /src/utils/database.ts: -------------------------------------------------------------------------------- 1 | import { getAnalytics, logEvent } from 'firebase/analytics'; 2 | import { initializeApp } from 'firebase/app'; 3 | import { 4 | set, 5 | getDatabase, 6 | ref, 7 | child, 8 | push, 9 | update, 10 | onChildAdded, 11 | onChildRemoved, 12 | DataSnapshot, 13 | onValue, 14 | startAt, 15 | query, 16 | orderByChild, 17 | } from 'firebase/database'; 18 | 19 | import { 20 | AnswerMessage, 21 | GameMessage, 22 | GameObj, 23 | isAnswerMessage, 24 | MessageNoticeObj, 25 | MessageObject, 26 | UserObj, 27 | Users, 28 | } from 'utils/types'; 29 | 30 | const firebaseConfig = { 31 | apiKey: process.env.REACT_APP_FIREBASE_API_KEY, 32 | authDomain: process.env.REACT_APP_FIREBASE_AUTH_DOMAIN, 33 | projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID, 34 | storageBucket: process.env.REACT_APP_FIREBASE_STORAGE_BUCKET, 35 | messagingSenderId: process.env.REACT_APP_FIREBASE_MESSAGING_SENDER_ID, 36 | appId: process.env.REACT_APP_FIREBASE_APP_ID, 37 | measurementId: process.env.REACT_APP_FIREBASE_MEASUREMENT_ID, 38 | databaseURL: process.env.REACT_APP_FIREBASE_DATABASE_URL, 39 | }; 40 | 41 | export const app = initializeApp(firebaseConfig); 42 | const database = getDatabase(app); 43 | 44 | const analytics = getAnalytics(); 45 | 46 | /* Message */ 47 | export const listenMessage = ( 48 | gameKey: string, 49 | callback: (data: MessageObject) => any, 50 | ) => { 51 | const gamesRef = ref(database, `Games/${gameKey}/messages`); 52 | const unSubscribe = onChildAdded(gamesRef, (data) => { 53 | let message; 54 | if (isAnswerMessage(data.val())) { 55 | message = data.val() as AnswerMessage; 56 | } else { 57 | message = data.val() as GameMessage; 58 | if (message.type === 'end' && unSubscribe) unSubscribe(); 59 | } 60 | callback({ 61 | ...message, 62 | key: data.key, 63 | }); 64 | }); 65 | }; 66 | 67 | export const pushMessage = ( 68 | gameKey: string, 69 | message: MessageObject, 70 | ): string => { 71 | const newMessageKey = 72 | push(child(ref(database), `Games/${gameKey}/messages`)).key ?? ''; 73 | const updates: { [key: string]: any } = {}; 74 | updates[`Games/${gameKey}/messages/${newMessageKey}`] = message; 75 | 76 | update(ref(database), updates) 77 | .then(() => { 78 | logEvent(analytics, 'send_message', { type: message.type }); 79 | }) 80 | .catch((err) => { 81 | alert('エラー:メッセージを書き込みできませんでした'); 82 | console.error(err); 83 | }); 84 | 85 | return newMessageKey; 86 | }; 87 | 88 | /* Game */ 89 | 90 | export const writeNewGame = (game: GameObj): void => { 91 | const newGameKey = push(child(ref(database), 'Games')).key ?? ''; 92 | const updates: { [key: string]: any } = {}; 93 | updates[`Games/${newGameKey}`] = game; 94 | 95 | update(ref(database), updates) 96 | .then(() => { 97 | pushMessage(newGameKey, { 98 | type: 'start', 99 | value: `${game.startBy}が\nゲーム開始\n参加者${game.users.length}人`, 100 | }); 101 | }) 102 | .catch((err) => { 103 | alert('エラー:ゲームを書き込みできませんでした'); 104 | console.error(err); 105 | }); 106 | }; 107 | 108 | export const listenGame = ( 109 | userKey: string, 110 | callback: (data: DataSnapshot) => any, 111 | ) => { 112 | const gamesRef = ref(database, 'Games/'); 113 | onChildAdded(gamesRef, (data) => { 114 | const gameData = data.val() as GameObj; 115 | if (gameData.users && gameData.users.includes(userKey)) callback(data); 116 | }); 117 | }; 118 | 119 | export const listenGameDeleted = (gameKey: string, callback: () => any) => { 120 | const gamesRef = ref(database, `Games/${gameKey}`); 121 | onChildRemoved(gamesRef, () => { 122 | callback(); 123 | }); 124 | }; 125 | 126 | export const deleteGame = (gameKey: string): void => { 127 | set(ref(database, `Games/${gameKey}`), null).catch((err) => { 128 | alert('エラー:ゲームを終了できませんでした'); 129 | console.error(err); 130 | }); 131 | }; 132 | 133 | /* User */ 134 | 135 | export const newOnlineUser = (user: UserObj): string => { 136 | let userKey = localStorage.getItem('prizm-userkey'); 137 | if (!userKey) { 138 | userKey = push(child(ref(database), 'Users')).key ?? ''; 139 | localStorage.setItem('prizm-userkey', userKey); 140 | } 141 | const updates: { [key: string]: any } = {}; 142 | updates[`Users/${userKey}`] = user; 143 | 144 | update(ref(database), updates).catch((err) => { 145 | alert('エラー:ユーザーを書き込みできませんでした'); 146 | console.error(err); 147 | }); 148 | 149 | return userKey; 150 | }; 151 | 152 | export const deleteUser = (userKey: string): void => { 153 | set(ref(database, `Users/${userKey}`), null).catch((err) => { 154 | alert('エラー:ユーザーを削除できませんでした'); 155 | console.error(err); 156 | }); 157 | }; 158 | 159 | export const listenUsers = (callback: (users: Users) => any) => { 160 | // const users = ref(database, 'Users/'); 161 | const users = query( 162 | ref(database, 'Users/'), 163 | orderByChild('pingStamp'), 164 | startAt(Date.now() - 7700), 165 | ); 166 | onValue(users, (data) => { 167 | callback(data.val() as Users); 168 | }); 169 | }; 170 | 171 | export const updatePingStamp = (userKey: string): void => { 172 | const updates: { [key: string]: any } = {}; 173 | updates[`Users/${userKey}/pingStamp`] = Date.now(); 174 | 175 | void update(ref(database), updates); 176 | }; 177 | 178 | /* log */ 179 | 180 | export const logUpdateName = (name: string, color: string) => { 181 | logEvent(analytics, 'update_name', { 182 | name, 183 | color, 184 | }); 185 | }; 186 | 187 | export const logMatched = (notice: MessageNoticeObj) => { 188 | logEvent(analytics, 'post_score', { score: notice.a_score, ...notice }); 189 | }; 190 | 191 | export const logStartGame = ( 192 | answer: string, 193 | mode: string, 194 | usersLength: number, 195 | startBy: string, 196 | ) => { 197 | logEvent(analytics, 'start_game', { 198 | answer, 199 | mode, 200 | usersLength, 201 | startBy, 202 | }); 203 | }; 204 | 205 | export const logGameCanceled = () => { 206 | logEvent(analytics, 'game_canceled'); 207 | }; 208 | 209 | export const logPageView = (title: string) => { 210 | logEvent(analytics, 'page_view', { page_title: title }); 211 | }; 212 | -------------------------------------------------------------------------------- /src/utils/gethint.ts: -------------------------------------------------------------------------------- 1 | import { hint, area } from 'assets/data/prefecture'; 2 | import { PrefectureStr } from 'utils/types'; 3 | 4 | const getHint = (progress: number, pref: PrefectureStr | undefined): string => { 5 | if (!pref) return ''; 6 | 7 | if (Math.floor(progress) === 83) return `地方は\n『${area[hint[pref][3]]}』`; 8 | if (Math.floor(progress) === 66) 9 | return `県庁所在地の頭文字は\n『${hint[pref][2]}』`; 10 | // if (Math.floor(progress) === 50) return `総面積は\n『${hint[pref][1]}km2』`; 11 | // if (Math.floor(progress) === 33) return `人口は\n約${hint[pref][0]}万人`; 12 | 13 | return ''; 14 | }; 15 | 16 | export default getHint; 17 | -------------------------------------------------------------------------------- /src/utils/summary.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GameObj, 3 | gameTimerSeconds, 4 | isUserSummaryObj, 5 | localUserSummary, 6 | MessageNoticeObj, 7 | modesDisplayWithEmoji, 8 | UserSummaryObj, 9 | UserSummaryObjOnStore, 10 | } from 'utils/types'; 11 | 12 | export const getSummary = (): false | UserSummaryObj => { 13 | const summary = JSON.parse( 14 | localStorage.getItem(localUserSummary) ?? '', 15 | ) as unknown; 16 | if (!isUserSummaryObj(summary)) { 17 | alert('エラー: 勝敗を記録できません'); 18 | 19 | return false; 20 | } 21 | 22 | return summary; 23 | }; 24 | 25 | export const updateSummary = (newSummary: { [key: string]: number }) => { 26 | const summary = getSummary(); 27 | if (summary) { 28 | localStorage.setItem( 29 | localUserSummary, 30 | JSON.stringify({ ...summary, ...newSummary }), 31 | ); 32 | } 33 | }; 34 | 35 | export const updateSummaryFromKey = ( 36 | key: keyof UserSummaryObj, 37 | value: number | ((number: number) => number), 38 | ) => { 39 | const summary = getSummary(); 40 | if (!summary) return false; 41 | 42 | summary[key] = typeof value === 'number' ? value : value(summary[key] ?? 0); 43 | localStorage.setItem(localUserSummary, JSON.stringify(summary)); 44 | 45 | return true; 46 | }; 47 | 48 | export const getNoticesWhenMatched = (lastWon: number): MessageNoticeObj => { 49 | const notice: MessageNoticeObj = {}; 50 | 51 | const summary = getSummary(); 52 | if (summary) { 53 | const currentStreak = summary.currentStreak + 1; 54 | const maxStreak = 55 | currentStreak > summary.maxStreak ? currentStreak : summary.maxStreak; 56 | 57 | if (currentStreak > 1) notice.c_update_streak = currentStreak; 58 | if (currentStreak > summary.maxStreak) 59 | notice.d_update_max_streak = currentStreak; 60 | 61 | const speed = 62 | Math.round(((Date.now() - lastWon) / 1000 - gameTimerSeconds * 3) * 10) / 63 | 10; 64 | const averageSpeed = 65 | summary.averageSpeed === 0 66 | ? speed 67 | : Math.round( 68 | ((summary.averageSpeed * (summary.playCount - 1) + speed) / 69 | summary.playCount) * 70 | 10, 71 | ) / 10; 72 | const fastestSpeed = 73 | speed < summary.fastestSpeed || summary.fastestSpeed === 0 74 | ? speed 75 | : summary.fastestSpeed; 76 | 77 | if ( 78 | speed < summary.fastestSpeed || 79 | (summary.fastestSpeed === 0 && summary.playCount > 1) 80 | ) 81 | notice.b_update_fastest = speed; 82 | 83 | updateSummary({ 84 | wonCount: summary.wonCount + 1, 85 | lastWon, 86 | currentStreak, 87 | maxStreak, 88 | averageSpeed, 89 | fastestSpeed, 90 | lastSpeed: speed, 91 | }); 92 | } 93 | 94 | return notice; 95 | }; 96 | 97 | export const generateTweet = ( 98 | gameObj: GameObj, 99 | summary: Partial, 100 | ) => { 101 | let tweet = '#prizmgame で勝利!💮\n'; 102 | 103 | if (summary && summary.a_score) tweet += `💯 score +${summary.a_score}\n`; 104 | tweet += `🗾 ${gameObj.answer}\n`; 105 | tweet += `⏩ ${modesDisplayWithEmoji[gameObj.mode]}\n`; 106 | tweet += `👥 参加者${gameObj.users.length}人\n`; 107 | if (summary && summary.lastSpeed) 108 | tweet += `⏱️ ${summary.lastSpeed}秒で回答💨\n`; 109 | if (summary && summary.currentStreak && summary.currentStreak > 1) 110 | tweet += `👍 ${summary.currentStreak}連勝中❗️\n\n`; 111 | 112 | if (summary && summary.d_update_max_streak && summary.d_update_max_streak > 1) 113 | tweet += `🎖️ 連勝記録を更新🔺\n`; 114 | if (summary && summary.b_update_fastest) tweet += `🎖️ 最速記録を更新🔺`; 115 | 116 | // tweet += '\nhttps://prizm.pw'; 117 | 118 | return tweet; 119 | }; 120 | -------------------------------------------------------------------------------- /src/utils/types.tsx: -------------------------------------------------------------------------------- 1 | import { prefecture } from 'assets/data/prefecture'; 2 | import { ReactNode } from 'react'; 3 | import { MdOutlineTrain } from 'react-icons/md'; 4 | import { 5 | TbMoodHappy, 6 | TbMoodSad, 7 | TbMoodSadDizzy, 8 | TbMoodWrrr, 9 | TbMoodXd, 10 | TbSkull, 11 | TbBolt, 12 | TbMountain, 13 | TbArrowsRandom, 14 | TbBuildingCastle, 15 | TbCar, 16 | TbBulb, 17 | TbBuildingBank, 18 | TbGrill, 19 | TbCookieMan, 20 | TbApple, 21 | TbSchool, 22 | TbConfetti, 23 | TbTrees, 24 | TbCookie, 25 | TbGhost2, 26 | } from 'react-icons/tb'; 27 | import IconSpa from 'assets/icon/spa'; 28 | 29 | /* GAME mode */ 30 | const modes = [ 31 | 'easy', 32 | 'normal', 33 | 'hard', 34 | 'random', 35 | 'hell', 36 | 'veryhell', 37 | 'veryveryhell', 38 | 'veryveryveryhell', 39 | 'mixed', 40 | 'station', 41 | 'mountain', 42 | 'castle', 43 | 'reststop', 44 | 'sweets', 45 | 'museum', 46 | 'festival', 47 | 'cuisine', 48 | 'attraction', 49 | 'powerplant', 50 | 'spa', 51 | 'specialty', 52 | 'goods', 53 | 'quiz', 54 | ] as const; 55 | export type Mode = typeof modes[number]; 56 | 57 | export const modesDisplay: { [key in Mode]: ReactNode } = { 58 | easy: ( 59 | 60 | 61 | 初級 62 | 63 | ), 64 | normal: ( 65 | 66 | 67 | 中級 68 | 69 | ), 70 | hard: ( 71 | 72 | 73 | 上級 74 | 75 | ), 76 | random: ( 77 | 78 | 79 | おまかせ 80 | 81 | ), 82 | hell: ( 83 | 84 | 85 | ゲキムズ 86 | 87 | ), 88 | veryhell: ( 89 | 90 | 91 | 超ムズ 92 | 93 | ), 94 | veryveryhell: ( 95 | 96 | 97 | 超激ムズ 98 | 99 | ), 100 | veryveryveryhell: ( 101 | 102 | 103 | 地獄 104 | 105 | ), 106 | mixed: ( 107 | 108 | 109 | ごちゃまぜ 110 | 111 | ), 112 | station: ( 113 | 114 | 115 | 116 | 117 | ), 118 | mountain: ( 119 | 120 | 121 | 122 | 123 | ), 124 | castle: ( 125 | 126 | 127 | 128 | 129 | ), 130 | reststop: ( 131 | 132 | 133 | 道の駅 134 | 135 | ), 136 | goods: ( 137 | 138 | 139 | 伝統工芸品 140 | 141 | ), 142 | specialty: ( 143 | 144 | 145 | 特産品 146 | 147 | ), 148 | quiz: ( 149 | 150 | 151 | 雑学 152 | 153 | ), 154 | cuisine: ( 155 | 156 | 157 | 郷土料理 158 | 159 | ), 160 | attraction: ( 161 | 162 | 163 | 名所 164 | 165 | ), 166 | sweets: ( 167 | 168 | 169 | 銘菓 170 | 171 | ), 172 | museum: ( 173 | 174 | 175 | 博物館 176 | 177 | ), 178 | spa: ( 179 | 180 | 181 | 温泉 182 | 183 | ), 184 | festival: ( 185 | 186 | 187 | お祭り 188 | 189 | ), 190 | powerplant: ( 191 | 192 | 193 | 発電所 194 | 195 | ), 196 | }; 197 | export const modesDisplayWithEmoji: { [key in Mode]: string } = { 198 | easy: '初級🔰', 199 | normal: '中級❤️‍🔥', 200 | hard: '上級😈', 201 | random: 'おまかせ', 202 | hell: 'ゲキムズ👹', 203 | veryhell: '超ムズ👹👹', 204 | veryveryhell: '超激ムズ☠️☠️', 205 | veryveryveryhell: '地獄👻', 206 | station: '駅モード🚉', 207 | mountain: '山モード⛰', 208 | castle: '城モード🏯', 209 | reststop: '道の駅🚗', 210 | museum: '博物館🖼', 211 | festival: 'お祭り👘', 212 | cuisine: '郷土料理🥘', 213 | attraction: '名所🚠', 214 | powerplant: '発電所🔌💡', 215 | spa: '温泉♨️', 216 | specialty: '特産品🍎', 217 | sweets: '銘菓🍘', 218 | goods: '伝統工芸品🪆', 219 | quiz: '雑学🎓', 220 | mixed: 'ごちゃまぜ🌀', 221 | }; 222 | export const modesCaption: { [key in Mode]: string } = { 223 | easy: '市町村が出題されます: ●●●', 224 | normal: '市町村の冒頭2文字が出題されます: ●●○', 225 | hard: '市町村の頭文字が出題されます: ●○○', 226 | hell: '市町村の2文字目が出題されます: ○●○', 227 | veryhell: '市町村の最後の字が出題されます: ○○●', 228 | veryveryhell: '市町村から任意の1文字を出題: ○○○→●', 229 | veryveryveryhell: '市町村から任意の1文字かつ3分の1が隠れた状態', 230 | station: '駅が出題されます', 231 | mountain: '山が出題されます', 232 | castle: '城が出題されます', 233 | reststop: '道の駅(東京,神奈川はPA/SAを含む)', 234 | sweets: '銘菓が出題されます', 235 | museum: '博物館が出題されます', 236 | festival: 'お祭りが出題されます', 237 | cuisine: '郷土料理が出題されます', 238 | attraction: '名所が出題されます', 239 | powerplant: '発電所が出題されます', 240 | spa: '温泉が出題されます', 241 | specialty: '特産品が出題されます', 242 | goods: '伝統工芸品が出題されます', 243 | quiz: '雑学が出題されます', 244 | random: 'モードがランダムで選択されます', 245 | mixed: '1問ごとに違うモードになります', 246 | }; 247 | export const modesDetail: { [key in Mode]: string } = { 248 | easy: '初級:市町村', 249 | normal: '中級:市町村の冒頭2文字', 250 | hard: '上級:市町村の頭文字', 251 | hell: 'ゲキムズ:市町村の2文字目', 252 | veryhell: '超ムズ:市町村の最後の字', 253 | veryveryhell: '超激ムズ:市町村の任意の字', 254 | veryveryveryhell: '地獄:市町村の任意の字', 255 | station: '駅モード', 256 | mountain: '山モード', 257 | castle: '城モード', 258 | reststop: '道の駅・PA', 259 | museum: '博物館', 260 | festival: 'お祭り', 261 | cuisine: '郷土料理', 262 | attraction: '名所', 263 | powerplant: '発電所', 264 | sweets: '銘菓モード', 265 | spa: '温泉モード', 266 | specialty: '特産品', 267 | goods: '伝統工芸品', 268 | quiz: '雑学モード', 269 | random: 'おまかせ', 270 | mixed: 'ごちゃまぜ', 271 | }; 272 | export const modesConvert: { [key in Mode]: (t: string) => string } = { 273 | easy: (t) => t, 274 | normal: (t) => t.substr(0, 2), 275 | hard: (t) => t.charAt(0), 276 | hell: (t) => t.charAt(1), 277 | veryhell: (t) => { 278 | if (t.length <= 2) return t.charAt(0); 279 | 280 | return t.charAt(t.length - 2); 281 | }, 282 | veryveryhell: (t) => { 283 | if (t.length <= 2) return t.charAt(0); 284 | 285 | const slicedString = t.slice(0, -1); 286 | const randomIndex = Math.floor(Math.random() * slicedString.length); 287 | 288 | return slicedString.charAt(randomIndex); 289 | }, 290 | veryveryveryhell: (t) => { 291 | if (t.length <= 2) return t.charAt(0); 292 | 293 | const slicedString = t.slice(0, -1); 294 | const randomIndex = Math.floor(Math.random() * slicedString.length); 295 | 296 | return slicedString.charAt(randomIndex); 297 | }, 298 | station: (t) => t, 299 | mountain: (t) => t, 300 | castle: (t) => t, 301 | reststop: (t) => t, 302 | museum: (t) => t, 303 | festival: (t) => t, 304 | cuisine: (t) => t, 305 | attraction: (t) => t, 306 | powerplant: (t) => t, 307 | spa: (t) => t, 308 | specialty: (t) => t, 309 | goods: (t) => t, 310 | quiz: (t) => t, 311 | sweets: (t) => t, 312 | random: (t) => t, 313 | mixed: (t) => t, 314 | }; 315 | export const modesScore: { [key in Mode]: (score: number) => number } = { 316 | easy: (score) => score * 0.75, 317 | normal: (score) => score, 318 | hard: (score) => score * 1.3, 319 | hell: (score) => score * 1.6, 320 | veryhell: (score) => score * 2, 321 | veryveryhell: (score) => score * 2.5, 322 | veryveryveryhell: (score) => score * 3, 323 | station: (score) => score, 324 | mountain: (score) => score, 325 | castle: (score) => score, 326 | reststop: (score) => score, 327 | museum: (score) => score, 328 | festival: (score) => score, 329 | cuisine: (score) => score, 330 | attraction: (score) => score, 331 | powerplant: (score) => score, 332 | spa: (score) => score, 333 | specialty: (score) => score, 334 | goods: (score) => score, 335 | sweets: (score) => score, 336 | quiz: (score) => score, 337 | random: (score) => score, 338 | mixed: (score) => score * 0.75, 339 | }; 340 | 341 | /* Game */ 342 | export type Questions = string[]; 343 | export type GameStatus = 'active' | 'systemWon' | 'userWon' | 'finished'; 344 | export type GameObj = { 345 | answer: PrefectureStr; 346 | questions: Questions; 347 | mode: Mode; 348 | messages: Messages; 349 | users: string[]; 350 | startBy: string; 351 | color: string; 352 | created: number; 353 | }; 354 | export type FinishGameFunction = ( 355 | isDeleted?: boolean, 356 | isUnset?: boolean, 357 | ) => void; 358 | export const gameTimerSeconds = 1.5; 359 | 360 | /* message */ 361 | export type MessageTypes = 'answer' | 'hint' | 'start' | 'score'; 362 | export type MessageNotice = 363 | | 'a_score' 364 | | 'b_update_fastest' 365 | | 'c_update_streak' 366 | | 'd_update_max_streak'; 367 | export type MessageNoticeObj = { [key in MessageNotice]?: number }; 368 | export type AnswerMessage = { 369 | key?: string | null; 370 | name: string; 371 | type: 'answer'; 372 | value: string; 373 | matched: boolean; 374 | }; 375 | export type GameMessage = { 376 | key?: string | null; 377 | type: 'hint' | 'start' | 'score' | 'remain' | 'end'; 378 | value: string; 379 | notice?: MessageNoticeObj; 380 | }; 381 | export type MessageObject = AnswerMessage | GameMessage; 382 | export type Messages = MessageObject[]; 383 | 384 | export const isAnswerMessage = (arg: unknown): arg is AnswerMessage => { 385 | const m = arg as AnswerMessage; 386 | 387 | return ( 388 | m?.type === 'answer' && 389 | typeof m?.name === 'string' && 390 | typeof m?.matched === 'boolean' 391 | ); 392 | }; 393 | export const isGameMessage = (arg: unknown): arg is GameMessage => { 394 | const m = arg as GameMessage; 395 | 396 | return typeof m?.value === 'string'; 397 | }; 398 | 399 | /* prefecture */ 400 | export type PrefectureStr = typeof prefecture[number]; 401 | export type HintObj = { 402 | [key in PrefectureStr]: string | number; 403 | }; 404 | export type DefinedQuestions = { 405 | [prefecture: string]: string[]; 406 | }; 407 | 408 | /* User */ 409 | export type UserDevice = 'mobile' | 'tablet' | 'desktop'; 410 | export type UserObj = { 411 | userName: string; 412 | pingStamp: number; 413 | color: string; 414 | device: UserDevice; 415 | score?: number; 416 | remain?: number; 417 | joiningGame?: string; 418 | }; 419 | export type Users = { 420 | [key: string]: UserObj; 421 | }; 422 | export const localScoreKey = 'prizm-score'; 423 | export const localUserNameKey = 'prizm-username'; 424 | export const localUserColorKey = 'prizm-usercolor'; 425 | export const localUserSummary = 'prizm-summary'; 426 | export const initialRemain = 3; 427 | 428 | export type UserSummaryObj = { 429 | playCount: number; 430 | wonCount: number; 431 | lastPlay: number; 432 | lastWon: number; 433 | currentStreak: number; 434 | maxStreak: number; 435 | averageSpeed: number; 436 | fastestSpeed: number; 437 | lastSpeed: number; 438 | }; 439 | export type UserSummaryObjOnStore = UserSummaryObj & MessageNoticeObj; 440 | export const isUserSummaryObj = (arg: unknown): arg is UserSummaryObj => { 441 | const m = arg as UserSummaryObj; 442 | 443 | return ( 444 | typeof m?.playCount === 'number' && 445 | typeof m?.wonCount === 'number' && 446 | typeof m?.lastPlay === 'number' && 447 | typeof m?.lastWon === 'number' && 448 | typeof m?.currentStreak === 'number' && 449 | typeof m?.maxStreak === 'number' && 450 | typeof m?.averageSpeed === 'number' && 451 | typeof m?.fastestSpeed === 'number' 452 | ); 453 | }; 454 | -------------------------------------------------------------------------------- /tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": [ 4 | "src/**/*.js", 5 | "src/**/*.jsx", 6 | "src/**/*.ts", 7 | "src/**/*.tsx" 8 | ], 9 | "exclude": [ 10 | "node_modules" 11 | ] 12 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx", 22 | "baseUrl": "src", 23 | "downlevelIteration": true, 24 | "types": ["@emotion/react/types/css-prop"], 25 | }, 26 | "include": [ 27 | "src" 28 | ] 29 | } 30 | --------------------------------------------------------------------------------