commit 73d619d8473eaf1e585cfc36bf90239df5b0b014
parent 4e06384a1523fb8bbcc4f85c78ce78fab1df7548
Author: Sunny <122193933+Sunny-JP@users.noreply.github.com>
Date: Sun, 7 Jun 2026 01:39:54 +0900
Merge pull request #63 from Sunny-JP/v5
V5
Diffstat:
14 files changed, 5461 insertions(+), 202 deletions(-)
diff --git a/package-lock.json b/package-lock.json
@@ -21,7 +21,8 @@
"react": "19.2.3",
"react-chartjs-2": "^5.3.1",
"react-dom": "19.2.3",
- "react-onesignal": "^3.4.4"
+ "react-onesignal": "^3.4.4",
+ "recharts": "^3.8.1"
},
"devDependencies": {
"@eslint/eslintrc": "^3.3.3",
@@ -2557,6 +2558,42 @@
"react-dom": ">=16.8.0"
}
},
+ "node_modules/@reduxjs/toolkit": {
+ "version": "2.11.2",
+ "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz",
+ "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@standard-schema/spec": "^1.0.0",
+ "@standard-schema/utils": "^0.3.0",
+ "immer": "^11.0.0",
+ "redux": "^5.0.1",
+ "redux-thunk": "^3.1.0",
+ "reselect": "^5.1.0"
+ },
+ "peerDependencies": {
+ "react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
+ "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
+ },
+ "peerDependenciesMeta": {
+ "react": {
+ "optional": true
+ },
+ "react-redux": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@reduxjs/toolkit/node_modules/immer": {
+ "version": "11.1.8",
+ "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.8.tgz",
+ "integrity": "sha512-/tbkHMW7y10Lx6i1crLjD4/OhNkRG+Fo7byZHtah0547nIeXYcpIXaUh0IAQY6gO5459qpGGYapcEOHtFXkIuA==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/immer"
+ }
+ },
"node_modules/@rollup/plugin-babel": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz",
@@ -2718,6 +2755,18 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@standard-schema/spec": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
+ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
+ "license": "MIT"
+ },
+ "node_modules/@standard-schema/utils": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
+ "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
+ "license": "MIT"
+ },
"node_modules/@supabase/auth-js": {
"version": "2.100.0",
"resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.100.0.tgz",
@@ -3180,6 +3229,69 @@
"tslib": "^2.4.0"
}
},
+ "node_modules/@types/d3-array": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
+ "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-color": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
+ "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-ease": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
+ "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-interpolate": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
+ "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-color": "*"
+ }
+ },
+ "node_modules/@types/d3-path": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
+ "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-scale": {
+ "version": "4.0.9",
+ "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
+ "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-time": "*"
+ }
+ },
+ "node_modules/@types/d3-shape": {
+ "version": "3.1.8",
+ "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz",
+ "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-path": "*"
+ }
+ },
+ "node_modules/@types/d3-time": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
+ "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-timer": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
+ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
+ "license": "MIT"
+ },
"node_modules/@types/eslint": {
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz",
@@ -3234,7 +3346,7 @@
"version": "19.2.14",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"csstype": "^3.2.2"
@@ -3262,6 +3374,12 @@
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT"
},
+ "node_modules/@types/use-sync-external-store": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
+ "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
+ "license": "MIT"
+ },
"node_modules/@types/ws": {
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
@@ -4610,6 +4728,15 @@
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
"license": "MIT"
},
+ "node_modules/clsx": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -4698,9 +4825,130 @@
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
- "dev": true,
+ "devOptional": true,
"license": "MIT"
},
+ "node_modules/d3-array": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
+ "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
+ "license": "ISC",
+ "dependencies": {
+ "internmap": "1 - 2"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-color": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
+ "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-ease": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
+ "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-format": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz",
+ "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-interpolate": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
+ "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-path": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
+ "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-scale": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
+ "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2.10.0 - 3",
+ "d3-format": "1 - 3",
+ "d3-interpolate": "1.2.0 - 3",
+ "d3-time": "2.1.1 - 3",
+ "d3-time-format": "2 - 4"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-shape": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
+ "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-path": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
+ "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time-format": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
+ "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-time": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-timer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
+ "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/damerau-levenshtein": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
@@ -4795,6 +5043,12 @@
}
}
},
+ "node_modules/decimal.js-light": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
+ "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
+ "license": "MIT"
+ },
"node_modules/deep-is": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@@ -5115,6 +5369,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/es-toolkit": {
+ "version": "1.46.1",
+ "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.46.1.tgz",
+ "integrity": "sha512-5eNtXOs3tbfxXOj04tjjseeWkRWaoCjdEI+96DgwzZoe6c9juL49pXlzAFTI72aWC9Y8p7168g6XIKjh7k6pyQ==",
+ "license": "MIT",
+ "workspaces": [
+ "docs",
+ "benchmarks"
+ ]
+ },
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
@@ -5590,6 +5854,12 @@
"node": ">=0.10.0"
}
},
+ "node_modules/eventemitter3": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
+ "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
+ "license": "MIT"
+ },
"node_modules/events": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
@@ -6167,6 +6437,16 @@
"node": ">= 4"
}
},
+ "node_modules/immer": {
+ "version": "10.2.0",
+ "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz",
+ "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/immer"
+ }
+ },
"node_modules/import-fresh": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
@@ -6225,6 +6505,15 @@
"node": ">= 0.4"
}
},
+ "node_modules/internmap": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
+ "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/is-array-buffer": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
@@ -7886,7 +8175,6 @@
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
- "dev": true,
"license": "MIT"
},
"node_modules/react-onesignal": {
@@ -7899,6 +8187,74 @@
"npm": ">=6"
}
},
+ "node_modules/react-redux": {
+ "version": "9.2.0",
+ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
+ "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/use-sync-external-store": "^0.0.6",
+ "use-sync-external-store": "^1.4.0"
+ },
+ "peerDependencies": {
+ "@types/react": "^18.2.25 || ^19",
+ "react": "^18.0 || ^19",
+ "redux": "^5.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "redux": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/recharts": {
+ "version": "3.8.1",
+ "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.8.1.tgz",
+ "integrity": "sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg==",
+ "license": "MIT",
+ "workspaces": [
+ "www"
+ ],
+ "dependencies": {
+ "@reduxjs/toolkit": "^1.9.0 || 2.x.x",
+ "clsx": "^2.1.1",
+ "decimal.js-light": "^2.5.1",
+ "es-toolkit": "^1.39.3",
+ "eventemitter3": "^5.0.1",
+ "immer": "^10.1.1",
+ "react-redux": "8.x.x || 9.x.x",
+ "reselect": "5.1.1",
+ "tiny-invariant": "^1.3.3",
+ "use-sync-external-store": "^1.2.2",
+ "victory-vendor": "^37.0.2"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/redux": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
+ "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
+ "license": "MIT"
+ },
+ "node_modules/redux-thunk": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
+ "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "redux": "^5.0.0"
+ }
+ },
"node_modules/reflect.getprototypeof": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
@@ -8003,6 +8359,12 @@
"node": ">=0.10.0"
}
},
+ "node_modules/reselect": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
+ "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
+ "license": "MIT"
+ },
"node_modules/resolve": {
"version": "1.22.11",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
@@ -8804,6 +9166,12 @@
"integrity": "sha512-oB7yIimd8SuGptespDAZnNkzIz+NWaJCu2RMsbs4Wmp9zSDUM8Nhi3s2OOcqYuv3mN4hitXc8DVx+LyUmbUDiA==",
"license": "ISC"
},
+ "node_modules/tiny-invariant": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
+ "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
+ "license": "MIT"
+ },
"node_modules/tinyglobby": {
"version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
@@ -9225,6 +9593,37 @@
"punycode": "^2.1.0"
}
},
+ "node_modules/use-sync-external-store": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
+ "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/victory-vendor": {
+ "version": "37.3.6",
+ "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz",
+ "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==",
+ "license": "MIT AND ISC",
+ "dependencies": {
+ "@types/d3-array": "^3.0.3",
+ "@types/d3-ease": "^3.0.0",
+ "@types/d3-interpolate": "^3.0.1",
+ "@types/d3-scale": "^4.0.2",
+ "@types/d3-shape": "^3.1.0",
+ "@types/d3-time": "^3.0.0",
+ "@types/d3-timer": "^3.0.0",
+ "d3-array": "^3.1.6",
+ "d3-ease": "^3.0.1",
+ "d3-interpolate": "^3.0.1",
+ "d3-scale": "^4.0.2",
+ "d3-shape": "^3.1.0",
+ "d3-time": "^3.0.0",
+ "d3-timer": "^3.0.1"
+ }
+ },
"node_modules/watchpack": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz",
diff --git a/package.json b/package.json
@@ -23,7 +23,8 @@
"react": "19.2.3",
"react-chartjs-2": "^5.3.1",
"react-dom": "19.2.3",
- "react-onesignal": "^3.4.4"
+ "react-onesignal": "^3.4.4",
+ "recharts": "^3.8.1"
},
"devDependencies": {
"@eslint/eslintrc": "^3.3.3",
diff --git a/src/app/api/relationship/route.ts b/src/app/api/relationship/route.ts
@@ -0,0 +1,128 @@
+import { NextResponse } from 'next/server';
+import { createClient } from '@supabase/supabase-js';
+
+export const runtime = 'edge';
+
+export async function OPTIONS() {
+ return new NextResponse(null, {
+ status: 204,
+ headers: {
+ 'Access-Control-Allow-Origin': 'https://rabbit1.cc',
+ 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
+ 'Access-Control-Allow-Headers': 'Content-Type, Authorization',
+ },
+ });
+}
+
+export async function GET(request: Request) {
+ try {
+ const authHeader = request.headers.get('Authorization');
+ if (!authHeader) return NextResponse.json({ error: 'No token' }, { status: 401 });
+
+ const supabase = createClient(
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
+ { global: { headers: { Authorization: authHeader } } }
+ );
+
+ const { data: { user }, error: authError } = await supabase.auth.getUser();
+ if (authError || !user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+
+ const { data: relationships, error } = await supabase
+ .from('relationship')
+ .select('id, char_key, bond_level, recorded_at')
+ .eq('user_id', user.id)
+ .order('recorded_at', { ascending: false });
+
+ if (error) throw error;
+
+ return NextResponse.json(relationships || []);
+ } catch (error: any) {
+ console.error("GET API Error:", error);
+ return NextResponse.json({ error: error.message }, { status: 500 });
+ }
+}
+
+export async function POST(request: Request) {
+ try {
+ const body = await request.json();
+ const { char_key, bond_level, recorded_at } = body;
+ const authHeader = request.headers.get('Authorization');
+
+ if (!authHeader) return NextResponse.json({ error: 'No token' }, { status: 401 });
+
+ const supabase = createClient(
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
+ { global: { headers: { Authorization: authHeader } } }
+ );
+
+ const { data: { user }, error: authError } = await supabase.auth.getUser();
+ if (authError || !user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+
+ const { data: existingRecord, error: checkError } = await supabase
+ .from('relationship')
+ .select('id')
+ .eq('user_id', user.id)
+ .eq('char_key', char_key)
+ .eq('bond_level', bond_level)
+ .maybeSingle();
+
+ if (checkError) throw checkError;
+
+ if (existingRecord) {
+ return NextResponse.json(
+ { error: `RANK ${bond_level} は既に登録されています。` },
+ { status: 400 }
+ );
+ }
+
+ const { error } = await supabase
+ .from('relationship')
+ .insert({
+ user_id: user.id,
+ char_key,
+ bond_level,
+ recorded_at
+ });
+
+ if (error) throw error;
+
+ return NextResponse.json({ success: true });
+ } catch (error: any) {
+ console.error("POST API Error:", error);
+ return NextResponse.json({ error: error.message }, { status: 500 });
+ }
+}
+
+export async function DELETE(request: Request) {
+ try {
+ const body = await request.json();
+ const { id } = body;
+ const authHeader = request.headers.get('Authorization');
+
+ if (!authHeader) return NextResponse.json({ error: 'No token' }, { status: 401 });
+
+ const supabase = createClient(
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
+ { global: { headers: { Authorization: authHeader } } }
+ );
+
+ const { data: { user }, error: authError } = await supabase.auth.getUser();
+ if (authError || !user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+
+ const { error } = await supabase
+ .from('relationship')
+ .delete()
+ .eq('id', id)
+ .eq('user_id', user.id);
+
+ if (error) throw error;
+
+ return NextResponse.json({ success: true });
+ } catch (error: any) {
+ console.error("DELETE API Error:", error);
+ return NextResponse.json({ error: error.message }, { status: 500 });
+ }
+}+
\ No newline at end of file
diff --git a/src/app/gifts.png b/src/app/gifts.png
Binary files differ.
diff --git a/src/app/globals.css b/src/app/globals.css
@@ -1,7 +1,10 @@
@import "tailwindcss";
+/* ==========================================
+ 1. VARIABLES & THEMES (:root / .dark)
+ ========================================== */
:root {
- --background: #f0f8ff; /* わずかに青みのある白 */
+ --background: #f0f8ff;
--foreground: #1a2a3a;
--card: #ffffff;
--card-foreground: #1a2a3a;
@@ -23,6 +26,9 @@
--tap-bg-6: hsl(var(--tap-h) var(--tap-s) 73%);
--tap-bg-7: hsl(var(--tap-h) var(--tap-s) 66%);
--tap-bg-8: hsl(var(--tap-h) var(--tap-s) 59%);
+ --system-color-scheme: light;
+ --svh: 100vh;
+ --bottom-nav-height: 56px;
}
.dark {
@@ -40,48 +46,48 @@
--tap-h: 235;
--tap-s: 40%;
--tap-bg-0: #0f0e1a;
- --tap-bg-8: hsl(var(--tap-h) var(--tap-s) 56%);
- --tap-bg-7: hsl(var(--tap-h) var(--tap-s) 50%);
- --tap-bg-6: hsl(var(--tap-h) var(--tap-s) 41%);
- --tap-bg-5: hsl(var(--tap-h) var(--tap-s) 34%);
- --tap-bg-4: hsl(var(--tap-h) var(--tap-s) 27%);
- --tap-bg-3: hsl(var(--tap-h) var(--tap-s) 23%);
- --tap-bg-2: hsl(var(--tap-h) var(--tap-s) 16%);
--tap-bg-1: hsl(var(--tap-h) var(--tap-s) 12%);
+ --tap-bg-2: hsl(var(--tap-h) var(--tap-s) 16%);
+ --tap-bg-3: hsl(var(--tap-h) var(--tap-s) 23%);
+ --tap-bg-4: hsl(var(--tap-h) var(--tap-s) 27%);
+ --tap-bg-5: hsl(var(--tap-h) var(--tap-s) 34%);
+ --tap-bg-6: hsl(var(--tap-h) var(--tap-s) 41%);
+ --tap-bg-7: hsl(var(--tap-h) var(--tap-s) 50%);
+ --tap-bg-8: hsl(var(--tap-h) var(--tap-s) 56%);
+ --system-color-scheme: dark;
}
-html {
- font-size: clamp(0.875rem, 0.5rem + 1vw, 1.125rem);
-}
-
-:root {
- --svh: 100vh;
- --bottom-nav-height: 56px;
-}
@supports (height: 1svh) {
:root {
--svh: 100svh;
}
}
-@media (min-width: 1000px) {
- :root {
- --bottom-nav-height: 0px;
- }
+
+/* ==========================================
+ 2. GLOBAL RESET & BASE STYLES
+ ========================================== */
+*,
+*::before,
+*::after {
+ -ms-overflow-style: none !important;
+ scrollbar-width: none !important;
+}
+
+*::-webkit-scrollbar {
+ display: none !important;
+}
+
+html {
+ font-size: clamp(0.875rem, 0.5rem + 1vw, 1.125rem);
}
html,
body,
#__next {
- height: var(--svh);
- min-height: var(--svh);
display: flex;
flex-direction: column;
-}
-
-header {
- background-color: var(--background);
- font-size: 1.35rem;
- font-weight: 700;
+ height: var(--svh);
+ min-height: var(--svh);
}
body {
@@ -92,10 +98,73 @@ body {
padding-bottom: calc(var(--bottom-nav-height));
}
+header {
+ background-color: var(--background);
+ font-size: 1.35rem;
+ font-weight: 700;
+}
+
+/* ==========================================
+ 3. COMMON UI COMPONENTS
+ ========================================== */
+/* Forms & Inputs */
+input[type="range"] {
+ appearance: none;
+ -webkit-appearance: none;
+ background: transparent;
+ cursor: pointer;
+}
+input[type="range"]::-webkit-slider-thumb {
+ appearance: none;
+ -webkit-appearance: none;
+ height: 18px;
+ width: 18px;
+ border-radius: 50%;
+ background-color: white;
+ border: 4px solid var(--tap-bg-8);
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
+}
+input[type="range"]::-moz-range-thumb {
+ height: 18px;
+ width: 18px;
+ border-radius: 50%;
+ background-color: white;
+ border: 4px solid var(--tap-bg-8);
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
+}
+
+/* Layout Containers */
+.dashboard-container {
+ padding: 4cqw 5cqw 2cqw 5cqw;
+}
+.cal-container {
+ padding: 2cqw 3cqw 2cqw 3cqw;
+}
+.bond-container {
+ padding: 5cqw 5cqw calc(var(--bottom-nav-height) - 4cqw) 5cqw;
+}
+
+.view-panel {
+ display: none;
+ width: 100%;
+}
+.view-panel.active {
+ display: block;
+}
+.mobile-view {
+ display: block;
+ margin-top: 3rem;
+ padding: 0.5rem;
+}
+.desktop-view,
+.desktop-hidden {
+ display: none;
+}
+
+/* Buttons */
.btn-setting {
flex: 1 1 0%;
- padding-top: 8px;
- padding-bottom: 8px;
+ padding: 8px 0;
text-align: center;
font-size: 1.2rem;
line-height: 1.25;
@@ -121,8 +190,7 @@ body {
flex-direction: column;
align-items: center;
justify-content: center;
- padding-top: 14px;
- padding-bottom: 18px;
+ padding: 14px 0 18px 0;
color: var(--nav-foreground);
background-color: var(--background);
transition: all 0.2s ease-in-out;
@@ -136,12 +204,10 @@ body {
stroke-width: 2.2;
}
.btn-nav:hover {
- color: var(--primary);
- opacity: 50%;
+ color: color-mix(in srgb, var(--primary) 65%, transparent);
}
.dark .btn-nav:hover {
- color: var(--foreground);
- opacity: 90%;
+ color: color-mix(in srgb, var(--foreground) 90%, transparent);
}
.btn-nav.active,
.btn-nav:hover.active {
@@ -152,14 +218,16 @@ body {
.dark .btn-nav:hover.active {
color: var(--foreground);
}
+
.btn-nav-bar {
+ position: relative;
+ bottom: -8px;
height: 2.5px;
width: 0px;
opacity: 0;
transition:
width 0.2s ease-out,
opacity 0.2s ease-out;
- bottom: -8px;
}
.btn-nav-bar.active {
background-color: var(--primary);
@@ -168,53 +236,41 @@ body {
}
.dark .btn-nav-bar.active {
background-color: var(--foreground);
- width: 50px;
- opacity: 1;
}
-.dashboard-container {
- container-type: inline-size;
+
+/* ==========================================
+ 4. APP MODULES (Timer, Calendar, Bond)
+ ========================================== */
+/* --- Timer Module --- */
+.timer-card {
display: flex;
flex-direction: column;
- gap: 6cqw;
- padding: 6cqw;
-}
-.cal-container {
- container-type: inline-size;
+ justify-content: center;
padding: 1cqw;
-}
-
-.timer-card {
+ margin-bottom: 4cqw;
color: var(--card-foreground);
border-radius: 10px;
- padding: 1cqw;
- margin-bottom: 2cqw;
backdrop-filter: blur(6px);
- display: flex;
- flex-direction: column;
- justify-content: center;
}
.timer-card-title {
- font-size: 4cqw;
+ font-size: 3cqw;
font-weight: 700;
color: var(--secondary-foreground);
margin-bottom: 0.8cqw;
+ letter-spacing: 0.02em;
}
-
.countdown-text-l {
- font-size: 14cqw;
- font-weight: 700;
+ font-size: 13cqw;
letter-spacing: 0.05em;
line-height: 1.1;
margin-top: -1.6cqw;
margin-bottom: 1.6cqw;
}
.countdown-text-s {
- font-size: 8.5cqw;
- font-weight: 700;
+ font-size: 7.5cqw;
letter-spacing: 0.05em;
line-height: 1.1;
}
-
.slot-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
@@ -227,21 +283,21 @@ body {
flex-direction: column;
align-items: center;
justify-content: center;
- border-style: none;
min-height: 8cqw;
- transition: all 0.2s;
- line-height: 1.1;
+ border-style: none;
font-weight: 700;
+ line-height: 1.1;
clip-path: polygon(0 0, 88% 0, 100% 28%, 100% 100%, 0 100%);
+ transition: all 0.2s;
}
.slot-default {
background-color: var(--card);
color: var(--muted);
}
.slot-current {
+ position: relative;
background-color: var(--card);
color: var(--primary);
- position: relative;
text-decoration: none;
overflow: hidden;
}
@@ -277,23 +333,23 @@ body {
}
.btn-timer {
+ position: relative;
+ display: flex;
+ align-items: center;
+ justify-content: center;
width: 100%;
- font-weight: 700;
- padding: 3cqw 0;
+ height: auto;
+ padding: 2cqw 0;
margin: 0;
- font-size: 4.5cqw;
- transition: all 0.3s cubic-bezier(0.23, 1, 0.32, 1);
+ font-size: 3.5cqw;
+ font-weight: 700;
background-color: var(--primary);
color: var(--primary-foreground);
- height: auto;
- cursor: pointer;
border: none;
- transform: skewX(-10deg);
border-radius: 8px;
- position: relative;
- display: flex;
- align-items: center;
- justify-content: center;
+ transform: skewX(-10deg);
+ cursor: pointer;
+ transition: all 0.3s cubic-bezier(0.23, 1, 0.32, 1);
}
.btn-timer span,
.btn-timer div {
@@ -313,6 +369,7 @@ body {
background-color: var(--muted);
}
+/* --- Calendar Module --- */
.cal-info {
font-size: 6cqw;
font-weight: 700;
@@ -329,10 +386,10 @@ body {
font-size: 52cqw;
font-weight: 700;
}
+
.cal-nav {
flex: 1 1 0%;
- padding-top: 14px;
- padding-bottom: 14px;
+ padding: 14px 0;
text-align: center;
font-size: 4cqw;
line-height: 1.25;
@@ -365,6 +422,7 @@ body {
.dark .cal-cell {
text-shadow: none;
}
+
.bg-tap-0 {
background-color: var(--tap-bg-0);
}
@@ -393,80 +451,7 @@ body {
background-color: var(--tap-bg-8);
}
-input[type="range"] {
- -webkit-appearance: none;
- appearance: none;
- background: transparent;
- cursor: pointer;
-}
-input[type="range"]::-webkit-slider-thumb {
- -webkit-appearance: none;
- appearance: none;
- height: 18px;
- width: 18px;
- background-color: white;
- border: 4px solid var(--tap-bg-8);
- border-radius: 50%;
- box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
-}
-input[type="range"]::-moz-range-thumb {
- height: 18px;
- width: 18px;
- background-color: white;
- border: 4px solid var(--tap-bg-8);
- border-radius: 50%;
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
-}
-
-.mobile-view {
- display: block;
- margin-top: 3rem;
- padding: 0.5rem;
-}
-.desktop-view {
- display: none;
-}
-.desktop-hidden {
- display: flex;
-}
-
-@media (min-width: 1000px) {
- html {
- font-size: 0.875rem;
- }
- .mobile-view {
- display: none;
- }
- .desktop-view {
- display: grid;
- grid-template-columns: repeat(2, minmax(0, 1fr));
- gap: 1.5rem;
- margin-top: 2rem;
- padding: 1.5rem;
- }
- .desktop-hidden {
- display: none;
- }
- .dashboard-container {
- gap: 2cqw;
- padding: 3cqw;
- }
-
- .timer-card-title {
- font-size: 3cqw;
- }
- .countdown-text-l {
- font-size: 11cqw;
- }
- .countdown-text-s {
- font-size: 7.5cqw;
- }
- .btn-timer {
- padding: 2cqw 0;
- font-size: 3.5cqw;
- }
-}
-
+/* --- Glow Ring Effects --- */
.ba-ring-container {
overflow: visible;
}
@@ -495,3 +480,566 @@ input[type="range"]::-moz-range-thumb {
opacity: 0;
}
}
+
+/* --- Bond Dashboard --- */
+.bond-split-layout {
+ display: flex;
+ flex-direction: column;
+ gap: 5cqw;
+ width: 100%;
+}
+.bond-left-panel {
+ display: flex;
+ flex-direction: column;
+}
+.char-selector {
+ position: relative;
+ margin-bottom: 2cqw;
+}
+.char-selector-trigger {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ background: transparent;
+ border: none;
+ padding: 0;
+ cursor: pointer;
+}
+.char-selector-label,
+.input-label {
+ font-size: 3cqw;
+ letter-spacing: 0.02em;
+ margin-bottom: 0.8cqw;
+ color: var(--secondary-foreground);
+}
+.char-selector-name {
+ display: flex;
+ align-items: baseline;
+ gap: 2cqw;
+ margin-top: 0.4cqw;
+ margin-bottom: 8px;
+ font-size: 7cqw;
+ font-weight: 900;
+ letter-spacing: -0.025em;
+ line-height: 1;
+}
+.char-selector-arrow {
+ font-size: 3.5cqw;
+ color: var(--secondary-foreground);
+ transition: transform 0.3s ease;
+}
+.char-selector-arrow.open {
+ transform: rotate(180deg);
+}
+.char-dropdown {
+ position: absolute;
+ top: 100%;
+ left: 0;
+ margin-top: 0.5cqw;
+ width: 45cqw;
+ max-width: 16rem;
+ max-height: 50cqw;
+ overflow-y: auto;
+ z-index: 50;
+ background-color: var(--card);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ border-radius: 2cqw;
+}
+.char-dropdown-container {
+ padding: 1.5cqw;
+}
+.char-dropdown-item {
+ width: 100%;
+ text-align: left;
+ padding: 1.5cqw 3cqw;
+ border: none;
+ border-radius: 1.5cqw;
+ background: transparent;
+ color: var(--foreground);
+ font-weight: 700;
+ font-size: 2.5cqw;
+ cursor: pointer;
+}
+.char-dropdown-item.active {
+ background-color: var(--primary);
+ color: var(--primary-foreground);
+}
+.record-input-area {
+ display: flex;
+ align-items: flex-end;
+ justify-content: space-between;
+ gap: 4cqw;
+ border-bottom: 1px solid rgba(var(--muted), 0.2);
+ padding-bottom: 2cqw;
+}
+.input-group-container {
+ display: flex;
+ align-items: baseline;
+ gap: 4cqw;
+ flex: 1;
+}
+.input-field-wrapper {
+ display: flex;
+ flex-direction: column;
+}
+.input-content {
+ display: flex;
+ width: 15.5cqw;
+}
+.rank-prefix {
+ font-size: 3.5cqw;
+ font-weight: 900;
+ line-height: 1;
+ margin-top: 2.2cqw;
+}
+.rank-value {
+ display: inline-block;
+ width: 9cqw;
+ margin-left: 0.5cqw;
+ font-size: 6cqw;
+ font-weight: 900;
+ line-height: 1;
+}
+.spin-buttons-box {
+ display: inline-block;
+ vertical-align: baseline;
+ margin-left: 1.5cqw;
+}
+.spin-btn {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 4cqw;
+ height: 3.2cqw;
+ background-color: rgba(var(--muted), 0.3);
+ border: none;
+ font-size: 2cqw;
+ color: var(--foreground);
+ cursor: pointer;
+}
+.spin-btn.up {
+ border-radius: 0.8cqw 0.8cqw 0 0;
+ margin-bottom: 2px;
+}
+.spin-btn.down {
+ border-radius: 0 0 0.8cqw 0.8cqw;
+}
+.date-input-display {
+ width: 42cqw;
+ margin-top: -0.5cqw;
+ font-size: 6cqw;
+ font-weight: 700;
+ line-height: 1;
+ letter-spacing: 0.03em;
+ background: transparent;
+ border: none;
+ outline: none;
+ padding: 0;
+ color-scheme: var(--system-color-scheme);
+}
+.btn-bond-record {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ width: 18cqw;
+ height: 9cqw;
+ flex-shrink: 0;
+ font-weight: 700;
+ background-color: var(--primary);
+ color: var(--primary-foreground);
+ border: none;
+ border-radius: 1.5cqw;
+ transform: skewX(-10deg);
+ cursor: pointer;
+ transition: all 0.3s cubic-bezier(0.23, 1, 0.32, 1);
+}
+.btn-bond-record:hover:not(:disabled) {
+ filter: brightness(1.1);
+ transform: skewX(-10deg) translateX(5px);
+}
+.btn-bond-record:disabled {
+ background-color: var(--muted);
+ color: var(--muted-foreground);
+ cursor: not-allowed;
+}
+.btn-bond-record span {
+ font-size: 2cqw;
+ font-weight: 900;
+ letter-spacing: 0.15em;
+ transform: skewX(10deg);
+ margin-bottom: -0.5cqw;
+}
+.btn-bond-record div {
+ font-size: 3cqw;
+ line-height: 1;
+ transform: skewX(10deg);
+}
+.btn-bond-record-dialog {
+ width: 100%;
+ margin-top: 0.5rem;
+ padding: 0.75rem 0;
+ border-radius: 0.75rem;
+ font-weight: 700;
+ background-color: var(--primary);
+ color: var(--primary-foreground);
+ border: none;
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
+ cursor: pointer;
+ transition: all 0.3s cubic-bezier(0.23, 1, 0.32, 1);
+}
+.btn-bond-record-dialog:hover:not(:disabled) {
+ filter: brightness(1.1);
+ transform: translateX(5px);
+}
+.btn-bond-record-dialog:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+.section-wrapper {
+ margin-top: 4cqw;
+}
+.section-toggle-btn {
+ display: flex;
+ align-items: center;
+ gap: 1.5cqw;
+ margin-bottom: 1cqw;
+ font-size: 3cqw;
+ letter-spacing: 0.02em;
+ color: var(--secondary-foreground);
+ background: transparent;
+ border: none;
+ cursor: pointer;
+}
+.graph-container {
+ height: 55cqw;
+ min-height: 12rem;
+ width: 100%;
+ margin-top: 1.5cqw;
+}
+.graph-container * {
+ outline: none !important;
+}
+.graph-container .recharts-tooltip-item {
+ color: var(--primary) !important;
+}
+.dark .graph-container .recharts-tooltip-item {
+ color: var(--secondary-foreground) !important;
+}
+
+.gift-title {
+ font-size: 3cqw;
+ letter-spacing: 0.02em;
+ margin-bottom: 2cqw;
+ color: var(--secondary-foreground);
+}
+.gift-scroll-container {
+ display: flex;
+ flex-wrap: nowrap;
+ gap: 1.5cqw;
+ width: 100%;
+ overflow-x: auto;
+ padding-top: 0.5cqw;
+ padding-bottom: 2cqw;
+}
+.gift-item {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ flex-shrink: 0;
+ width: 50px;
+ height: 70px;
+}
+.gift-image-box {
+ position: relative;
+ width: 100%;
+ aspect-ratio: 1 / 1;
+ margin-bottom: 1cqw;
+}
+.gift-sprite {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ width: 142px;
+ height: 112px;
+ background-image: url("gifts.png");
+ background-repeat: no-repeat;
+ background-position: calc(var(--col) * -142px) calc(var(--row) * -112px);
+ transform: translate(-50%, -50%) scale(0.48);
+ transform-origin: center center;
+ image-rendering: -webkit-optimize-contrast;
+}
+.gift-rank-dot {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 2cqw;
+ height: 2cqw;
+ border-radius: 9999px;
+ border: 1px solid var(--background);
+ z-index: 10;
+}
+.gift-rank-dot.s {
+ background-color: #ef4444;
+}
+.gift-rank-dot.a {
+ background-color: #fb923c;
+}
+.gift-rank-dot.b {
+ background-color: #3b82f6;
+}
+
+.gift-category-text {
+ width: 100%;
+ font-size: 2cqw;
+ min-height: 2.5cqw;
+ text-align: center;
+ font-weight: 500;
+ line-height: 1;
+ overflow-x: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+}
+
+/* ==========================================
+ History Log Compact Grid Styles (5行×2列)
+ ========================================== */
+.history-grid-container {
+ display: grid;
+ grid-template-rows: repeat(5, minmax(0, 1fr));
+ grid-auto-flow: column;
+ gap: 0.125rem 1.5rem;
+ margin-bottom: 2cqw;
+}
+.history-item {
+ display: flex;
+ align-items: center;
+ padding: 0.15rem 0.5rem;
+ border-bottom: 1px solid rgba(var(--muted), 0.05);
+ min-height: 1.75rem;
+}
+.history-log-row {
+ font-size: 0.875rem;
+ letter-spacing: 0.02em;
+ display: flex;
+ align-items: center;
+}
+.history-rank-num {
+ display: inline-block;
+ width: 1.5rem;
+ text-align: right;
+ font-weight: 700;
+ color: var(--foreground);
+}
+.history-divider {
+ margin: 0 0.5rem;
+ opacity: 0.25;
+ color: var(--foreground);
+}
+.history-date-text {
+ color: var(--secondary-foreground);
+}
+.history-empty-text {
+ color: var(--muted-foreground);
+ opacity: 0.4;
+}
+.edit-icon-btn {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--secondary-foreground);
+ background: transparent;
+ border: none;
+ padding: 0.2rem;
+ margin-left: 0.3rem;
+ border-radius: 0.25rem;
+ cursor: pointer;
+ transition: all 0.2s ease;
+}
+.edit-icon-btn:hover {
+ color: var(--primary);
+ background-color: rgba(var(--muted), 0.1);
+}
+.edit-btn-placeholder {
+ display: inline-block;
+ width: 1.5rem;
+ height: 1px;
+ opacity: 0;
+ pointer-events: none;
+}
+
+/* ==========================================
+ 5. MEDIA QUERIES (Desktop Optimization)
+ ========================================== */
+@media (min-width: 1000px) {
+ html {
+ font-size: 0.875rem;
+ }
+
+ :root {
+ --bottom-nav-height: 56px;
+ }
+
+ .dashboard-container {
+ container-type: inline-size;
+ display: flex;
+ flex-direction: column;
+ gap: 2cqw;
+ padding: 2cqw 2cqw 2cqw 5cqw;
+ }
+ .cal-container {
+ container-type: inline-size;
+ padding: 2cqw 5cqw 2cqw 2cqw;
+ }
+ .bond-container {
+ container-type: inline-size;
+ display: flex;
+ flex-direction: column;
+ padding: 2cqw 3cqw 2cqw 3cqw;
+ }
+ .bond-split-layout {
+ flex-direction: row;
+ align-items: stretch;
+ gap: 4rem;
+ }
+ .bond-left-panel {
+ flex: 1.2;
+ display: flex;
+ flex-direction: column;
+ }
+ .bond-right-panel {
+ flex: 1.8;
+ display: flex;
+ flex-direction: column;
+ border-left: 1px solid rgba(var(--muted), 0.15);
+ }
+ .char-selector {
+ margin-bottom: 1cqw;
+ }
+ .char-selector-name {
+ font-size: 3.5cqw;
+ gap: 0.5cqw;
+ }
+ .char-selector-label,
+ .input-label {
+ font-size: 1.5cqw;
+ }
+ .char-selector-arrow {
+ font-size: 1.3cqw;
+ }
+ .char-dropdown {
+ width: 16rem;
+ max-height: 20rem;
+ border-radius: 1rem;
+ }
+ .char-dropdown-container {
+ padding: 0.5cqw;
+ }
+ .char-dropdown-item {
+ font-size: 14px;
+ padding: 6px 12px;
+ }
+ .record-input-area {
+ padding-bottom: 1.5rem;
+ }
+ .input-content {
+ display: flex;
+ width: 6cqw;
+ }
+ .rank-prefix {
+ font-size: 1.3cqw;
+ margin-top: 1cqw;
+ }
+ .rank-value {
+ display: inline-block;
+ width: 5cqw;
+ margin-left: 0.5cqw;
+ font-size: 2.5cqw;
+ font-weight: 900;
+ line-height: 1;
+ }
+ .spin-buttons-box {
+ display: inline-block;
+ vertical-align: baseline;
+ margin-left: 0.5cqw;
+ margin-top: 0.2cqw;
+ scale: 1.3;
+ }
+ .spin-btn {
+ width: 1cqw;
+ height: 1cqw;
+ font-size: 0.8cqw;
+ }
+ .date-input-display {
+ font-size: 2.5cqw;
+ width: 17.5cqw;
+ margin-top: -0.25cqw;
+ }
+ .btn-bond-record {
+ width: 7cqw;
+ height: 4cqw;
+ border-radius: 0.75rem;
+ margin-left: 11cqw;
+ }
+ .btn-bond-record span {
+ font-size: 1cqw;
+ margin-bottom: -0.1cqw;
+ }
+ .btn-bond-record div {
+ font-size: 1cqw;
+ }
+ .section-wrapper {
+ margin-top: 1cqw;
+ }
+ .section-toggle-btn,
+ .gift-title {
+ font-size: 1.5cqw;
+ margin-bottom: 0.5cqw;
+ }
+ .gift-scroll-container {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(4rem, 1fr));
+ gap: 0.5cqw 0;
+ overflow-x: visible;
+ }
+ .gift-image-box {
+ border-radius: 1rem;
+ margin-bottom: 0.5rem;
+ }
+ .gift-rank-dot {
+ width: 0.7cqw;
+ height: 0.7cqw;
+ }
+ .gift-category-text {
+ font-size: 0.7cqw;
+ }
+ .history-grid-container {
+ gap: 0.1cqw 2cqw;
+ }
+ .history-item {
+ padding: 0.1cqw 0.4cqw;
+ min-height: 1.5cqw;
+ }
+ .history-log-row {
+ font-size: 0.9cqw;
+ }
+ .history-rank-num {
+ width: 1.5cqw;
+ }
+ .history-divider {
+ margin: 0 0.4cqw;
+ }
+ .edit-icon-btn {
+ padding: 0.1cqw;
+ }
+ .edit-btn-placeholder {
+ width: 1.2cqw;
+ }
+ .graph-container {
+ flex: 1;
+ width: 100%;
+ height: 100% !important;
+ min-height: 25rem;
+ margin-top: 0.5cqw;
+ }
+}
diff --git a/src/app/page.tsx b/src/app/page.tsx
@@ -7,12 +7,14 @@ import Header from "@/components/Header";
import TimerDashboard from "@/components/TimerDashboard";
import BottomNavBar from "@/components/BottomNavBar";
import HistoryCalendar from "@/components/HistoryCalendar";
+import BondDashboard from "@/components/BondDashboard";
import Settings from "@/components/Settings";
import SidePanel from "@/components/SidePanel";
+import NewFeatureDialog from "@/components/NewFeatureDialog";
import { CALENDAR_LIMITS } from "@/lib/timeUtils";
import { OVERLAY_CONTENTS } from "@/components/pages";
-type Tab = 'timer' | 'history';
+type Tab = 'timer' | 'history' | 'bond';
const Overlay = ({ contentKey, onClose }: { contentKey: string; onClose: () => void }) => {
const content = OVERLAY_CONTENTS[contentKey];
@@ -45,13 +47,30 @@ const Overlay = ({ contentKey, onClose }: { contentKey: string; onClose: () => v
};
export default function Home() {
- const [activeTab, setActiveTab] = useState<Tab>('timer');
+ const [activeTab, setActiveTab] = useState<Tab>(() => {
+ if (typeof window !== 'undefined') {
+ const savedTab = localStorage.getItem('last_active_tab') as Tab | null;
+ const savedTimeStr = localStorage.getItem('last_active_tab_time');
+ if (savedTab === 'timer' || savedTab === 'history' || savedTab === 'bond') {
+ if (savedTimeStr) {
+ const savedTime = parseInt(savedTimeStr, 10);
+ const now = Date.now();
+ const TEN_MINUTES = 10 * 60 * 1000;
+ if (now - savedTime < TEN_MINUTES) {
+ return savedTab;
+ }
+ }
+ }
+ }
+ return 'timer';
+ });
const [session, setSession] = useState<any>(null);
const [timerHistory, setTimerHistory] = useState<number[]>([]);
const [calendarHistory, setCalendarHistory] = useState<number[]>([]);
const [calendarDate, setCalendarDate] = useState(() => new Date());
const [ticket1Time, setTicket1Time] = useState<Date | null>(null);
const [ticket2Time, setTicket2Time] = useState<Date | null>(null);
+ const [relationshipHistory, setRelationshipHistory] = useState<any[]>([]);
const [isSyncing, setIsSyncing] = useState(false);
const [isDataLoaded, setIsDataLoaded] = useState(false);
const [isAuthChecking, setIsAuthChecking] = useState(true);
@@ -60,6 +79,49 @@ export default function Home() {
const isInitialFetched = useRef(false);
+ useEffect(() => {
+ if (typeof window !== 'undefined' && activeTab) {
+ localStorage.setItem('last_active_tab', activeTab);
+ localStorage.setItem('last_active_tab_time', Date.now().toString());
+ }
+ }, [activeTab]);
+
+ const fetchRelationshipHistory = useCallback(async () => {
+ const { data: { session: s } } = await supabase.auth.getSession();
+ if (!s) return;
+ try {
+ const res = await fetch('/api/relationship', {
+ headers: { 'Authorization': `Bearer ${s.access_token}` }
+ });
+ const data = await res.json();
+ setRelationshipHistory(Array.isArray(data) ? data : []);
+ } catch (e) { console.error(e); }
+ }, []);
+
+ const handleSaveRelationship = async (level: number, date: string, charKey: string) => {
+ if (!session || isSyncing) return;
+ setIsSyncing(true);
+ try {
+ const res = await fetch('/api/relationship', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${session.access_token}` },
+ body: JSON.stringify({ char_key: charKey, bond_level: level, recorded_at: date })
+ });
+
+ if (!res.ok) {
+ const errorData = await res.json();
+ alert(errorData.error || "登録に失敗しました。");
+ return;
+ }
+
+ await fetchRelationshipHistory();
+ } catch (e) {
+ console.error(e);
+ } finally {
+ setIsSyncing(false);
+ }
+ };
+
const fetchMonthlyData = useCallback(async (year: number, month: number) => {
const { data: { session: s } } = await supabase.auth.getSession();
if (!s?.user?.id) return [];
@@ -102,7 +164,8 @@ export default function Home() {
setCalendarDate(new Date(year, month - 1, 1));
const [profileRes, monthlyData] = await Promise.all([
supabase.from('profiles').select('ticket1_time, ticket2_time').eq('id', s.user.id).single(),
- fetchMonthlyData(year, month)
+ fetchMonthlyData(year, month),
+ fetchRelationshipHistory()
]);
if (profileRes.data) {
if (profileRes.data.ticket1_time) setTicket1Time(new Date(profileRes.data.ticket1_time));
@@ -112,7 +175,7 @@ export default function Home() {
setTimerHistory(monthlyData);
setIsDataLoaded(true);
} finally { setIsAuthChecking(false); }
- }, [fetchMonthlyData]);
+ }, [fetchMonthlyData, fetchRelationshipHistory]);
useEffect(() => {
supabase.auth.getSession().then(({ data: { session: s } }) => {
@@ -211,11 +274,14 @@ export default function Home() {
<div className="bg-background h-screen flex flex-col">
<OneSignalInit />
<Header isLoggedIn={!!session} onMenuClick={() => setIsSidePanelOpen(true)} />
+ <NewFeatureDialog />
{overlayKey && <Overlay contentKey={overlayKey} onClose={() => setOverlayKey(null)} />}
- <main className="flex-1 flex flex-col pt-16 pb-16 min-[1000px]:pb-0">
- <div className="min-[1000px]:hidden flex-1">
+ <main className="flex-1 flex flex-col pt-16 pb-16">
+
+ {/* スマホ表示 (幅1000px未満) */}
+ <div className="min-[1000px]:hidden flex-1 overflow-y-auto">
{activeTab === 'timer' && (
<TimerDashboard tapHistory={timerHistory} lastTapTime={lastTapTime} ticket1Time={ticket1Time} ticket2Time={ticket2Time} onTap={handleTap} onInvite={handleInvite} isSyncing={isSyncing} isDataLoaded={isDataLoaded} />
)}
@@ -224,14 +290,33 @@ export default function Home() {
<HistoryCalendar tapHistory={calendarHistory} currentDate={calendarDate} onMonthChange={handleMonthChange} />
</div>
)}
+ {activeTab === 'bond' && (
+ <BondDashboard bondHistory={relationshipHistory} onSave={handleSaveRelationship} isSyncing={isSyncing} />
+ )}
</div>
- <div className="hidden min-[1000px]:flex flex-1 items-center justify-center p-6 h-[calc(100vh-64px)] overflow-hidden">
- <div className="grid grid-cols-2 gap-6 w-full max-w-[160svh] items-stretch mx-auto">
- <TimerDashboard tapHistory={timerHistory} lastTapTime={lastTapTime} ticket1Time={ticket1Time} ticket2Time={ticket2Time} onTap={handleTap} onInvite={handleInvite} isSyncing={isSyncing} isDataLoaded={isDataLoaded} />
- <HistoryCalendar tapHistory={calendarHistory} currentDate={calendarDate} onMonthChange={handleMonthChange} />
+
+ {/* PC表示 (幅1000px以上) */}
+ <div className="hidden min-[1000px]:flex flex-1 justify-center px-4 py-4 h-[calc(100vh-120px)] overflow-hidden">
+ <div className="w-full h-full mx-auto flex items-stretch">
+
+ {(activeTab === 'timer' || activeTab === 'history') && (
+ <div className="grid grid-cols-2 gap-8 w-full h-full items-stretch animate-in fade-in duration-200">
+ <TimerDashboard tapHistory={timerHistory} lastTapTime={lastTapTime} ticket1Time={ticket1Time} ticket2Time={ticket2Time} onTap={handleTap} onInvite={handleInvite} isSyncing={isSyncing} isDataLoaded={isDataLoaded} />
+ <HistoryCalendar tapHistory={calendarHistory} currentDate={calendarDate} onMonthChange={handleMonthChange} />
+ </div>
+ )}
+
+ {activeTab === 'bond' && (
+ <div className="w-full h-full overflow-y-auto animate-in fade-in">
+ <BondDashboard bondHistory={relationshipHistory} onSave={handleSaveRelationship} isSyncing={isSyncing} />
+ </div>
+ )}
+
</div>
</div>
+
<BottomNavBar activeTab={activeTab} setActiveTab={setActiveTab} />
+
<SidePanel isOpen={isSidePanelOpen} onClose={() => setIsSidePanelOpen(false)}>
<Settings onOpenContent={(key) => { setOverlayKey(key); setIsSidePanelOpen(false); }} />
</SidePanel>
diff --git a/src/components/BondDashboard.tsx b/src/components/BondDashboard.tsx
@@ -0,0 +1,613 @@
+"use client";
+
+import React, { useState, useMemo, useRef, useEffect, useLayoutEffect } from 'react';
+import {
+ XAxis, YAxis, CartesianGrid,
+ ResponsiveContainer, AreaChart, Area, Tooltip, Line
+} from 'recharts';
+import { format } from 'date-fns';
+import {
+ CHARACTER_LIST, GIFT_LIST,
+ SPRITE_CONFIG, CharacterId,
+ BOND_EXP_TABLE
+} from '@/lib/bondData';
+
+interface BondRecord {
+ id: string;
+ char_key: string;
+ bond_level: number;
+ recorded_at: string;
+}
+
+const MIN_CHART_DIMENSION = 32;
+
+const getLevelFromExp = (exp: number): number => {
+ let currentLevel = 1;
+ for (let lv = 1; lv <= 100; lv++) {
+ if (BOND_EXP_TABLE[lv] !== undefined && exp >= BOND_EXP_TABLE[lv]) {
+ currentLevel = lv;
+ } else {
+ break;
+ }
+ }
+ return currentLevel;
+};
+
+const TrashIcon = () => (
+ <svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
+ <path strokeLinecap="round" strokeLinejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
+ </svg>
+);
+
+export default function BondDashboard({
+ bondHistory,
+ onSave,
+ isSyncing
+}: {
+ bondHistory: BondRecord[],
+ onSave: (level: number, date: string, charKey: string) => Promise<void>,
+ isSyncing?: boolean
+}) {
+ const [selectedCharId, setSelectedCharId] = useState<CharacterId>(CHARACTER_LIST[0].id);
+ const [isMounted, setIsMounted] = useState(false);
+
+ const containerRef = useRef<HTMLDivElement | null>(null);
+ const [isChartReady, setIsChartReady] = useState(false);
+
+ useEffect(() => {
+ if (typeof window !== 'undefined') {
+ const savedCharId = localStorage.getItem('last_selected_char_id') as CharacterId | null;
+ if (savedCharId && CHARACTER_LIST.some(c => c.id === savedCharId)) {
+ setSelectedCharId(savedCharId);
+ }
+ setIsMounted(true);
+ }
+ }, []);
+
+ useLayoutEffect(() => {
+ const el = containerRef.current;
+ if (!el) return;
+
+ const update = (rect = el.getBoundingClientRect()) => {
+ if (rect.width >= MIN_CHART_DIMENSION && rect.height >= MIN_CHART_DIMENSION) {
+ setIsChartReady(true);
+ }
+ };
+
+ update();
+ if (typeof ResizeObserver === "undefined") return;
+
+ const observer = new ResizeObserver((entries) => {
+ for (const entry of entries) {
+ if (entry.target === el) update(entry.contentRect);
+ }
+ });
+ observer.observe(el);
+
+ return () => observer.disconnect();
+ }, []);
+
+ const [inputLevel, setInputLevel] = useState(1);
+ const [inputDate, setInputDate] = useState(format(new Date(), 'yyyy-MM-dd'));
+
+ const [isSelectOpen, setIsSelectOpen] = useState(false);
+ const [isDesktop, setIsDesktop] = useState(false);
+
+ const dropdownRef = useRef<HTMLDivElement>(null);
+
+ const selectedChar = useMemo(() =>
+ CHARACTER_LIST.find(c => c.id === selectedCharId) || CHARACTER_LIST[0],
+ [selectedCharId]);
+
+ const sortedGifts = useMemo(() => {
+ const ratingOrder: Record<string, number> = {
+ 'ss': 1, 'sa': 2, 'ns': 3, 'na': 4, 'nb': 5
+ };
+
+ return GIFT_LIST
+ .filter(gift => selectedChar.giftRatings[gift.name])
+ .sort((a, b) => {
+ const ratingA = selectedChar.giftRatings[a.name]?.toLowerCase() || '';
+ const ratingB = selectedChar.giftRatings[b.name]?.toLowerCase() || '';
+ const orderA = ratingOrder[ratingA] || 99;
+ const orderB = ratingOrder[ratingB] || 99;
+ return orderA - orderB;
+ });
+ }, [selectedChar]);
+
+ const filteredHistory = useMemo(() => {
+ return bondHistory
+ .filter(h => h.char_key === selectedCharId)
+ .sort((a, b) => b.recorded_at.localeCompare(a.recorded_at));
+ }, [bondHistory, selectedCharId]);
+
+ const displayHistoryRows = useMemo(() => {
+ const rows = [];
+ for (let i = 0; i < 10; i++) {
+ rows.push(filteredHistory[i] || null);
+ }
+ return rows;
+ }, [filteredHistory]);
+
+ const chartDataCombined = useMemo(() => {
+ if (filteredHistory.length === 0) return { data: [], hasPrediction: false };
+
+ const actualPoints = [...filteredHistory]
+ .reverse()
+ .map(h => ({
+ recorded_at: h.recorded_at,
+ timestamp: new Date(h.recorded_at).getTime(),
+ cumulative_exp: BOND_EXP_TABLE[h.bond_level] || 0,
+ bond_level: h.bond_level,
+ isPrediction: false,
+ actual_exp: BOND_EXP_TABLE[h.bond_level] || 0,
+ predicted_exp: null as number | null,
+ }));
+
+ const lastActual = actualPoints[actualPoints.length - 1];
+ const targetExp = 240225;
+
+ if (lastActual.cumulative_exp >= targetExp || actualPoints.length < 2) {
+ return { data: actualPoints, hasPrediction: false, xDomain: ['dataMin', 'dataMax'] as const };
+ }
+
+ const n = actualPoints.length;
+ let sumX = 0, sumY = 0, sumXY = 0, sumXX = 0;
+ for (const p of actualPoints) {
+ sumX += p.timestamp;
+ sumY += p.cumulative_exp;
+ sumXY += p.timestamp * p.cumulative_exp;
+ sumXX += p.timestamp * p.timestamp;
+ }
+
+ const denominator = n * sumXX - sumX * sumX;
+ if (denominator === 0) {
+ return { data: actualPoints, hasPrediction: false, xDomain: ['dataMin', 'dataMax'] as const };
+ }
+
+ const slope = (n * sumXY - sumX * sumY) / denominator;
+ const intercept = (sumY - slope * sumX) / n;
+
+ if (slope <= 0) {
+ return { data: actualPoints, hasPrediction: false, xDomain: ['dataMin', 'dataMax'] as const };
+ }
+
+ const predictionPoints: any[] = [];
+
+ predictionPoints.push({
+ recorded_at: lastActual.recorded_at,
+ timestamp: lastActual.timestamp,
+ cumulative_exp: lastActual.cumulative_exp,
+ bond_level: lastActual.bond_level,
+ isPrediction: true,
+ actual_exp: null,
+ predicted_exp: lastActual.cumulative_exp
+ });
+
+ let nextTargetLevel = Math.floor((lastActual.bond_level / 5) + 1) * 5;
+ if (nextTargetLevel === lastActual.bond_level) {
+ nextTargetLevel += 5;
+ }
+
+ for (let lv = nextTargetLevel; lv <= 100; lv += 5) {
+ const expAtLv = BOND_EXP_TABLE[lv];
+ if (expAtLv === undefined) continue;
+
+ const ts = Math.round((expAtLv - intercept) / slope);
+
+ predictionPoints.push({
+ recorded_at: format(new Date(ts), 'yyyy-MM-dd'),
+ timestamp: ts,
+ cumulative_exp: expAtLv,
+ bond_level: lv,
+ isPrediction: true,
+ actual_exp: null,
+ predicted_exp: expAtLv
+ });
+ }
+
+ const finalPoint = predictionPoints[predictionPoints.length - 1];
+ if (!finalPoint || finalPoint.bond_level !== 100) {
+ const ts100 = Math.round((targetExp - intercept) / slope);
+ predictionPoints.push({
+ recorded_at: format(new Date(ts100), 'yyyy-MM-dd'),
+ timestamp: ts100,
+ cumulative_exp: targetExp,
+ bond_level: 100,
+ isPrediction: true,
+ actual_exp: null,
+ predicted_exp: targetExp
+ });
+ }
+
+ return {
+ data: [...actualPoints, ...predictionPoints.slice(0)],
+ hasPrediction: true,
+ xDomain: [actualPoints[0].timestamp, predictionPoints[predictionPoints.length - 1].timestamp] as const
+ };
+ }, [filteredHistory]);
+
+ const graphData = chartDataCombined.data;
+
+ useEffect(() => {
+ if (!isMounted) return;
+ localStorage.setItem('last_selected_char_id', selectedCharId);
+ const charHistory = bondHistory
+ .filter(h => h.char_key === selectedCharId)
+ .sort((a, b) => b.recorded_at.localeCompare(a.recorded_at));
+ if (charHistory.length > 0) {
+ setInputLevel(Math.min(100, charHistory[0].bond_level + 1));
+ } else {
+ setInputLevel(1);
+ }
+ }, [selectedCharId, bondHistory, isMounted]);
+
+ useEffect(() => {
+ const handleResize = () => {
+ setIsDesktop(window.innerWidth >= 1000);
+ };
+ handleResize();
+ window.addEventListener('resize', handleResize);
+ return () => window.removeEventListener('resize', handleResize);
+ }, []);
+
+ useEffect(() => {
+ const handleClickOutside = (event: MouseEvent) => {
+ if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
+ setIsSelectOpen(false);
+ }
+ };
+ document.addEventListener('mousedown', handleClickOutside);
+ return () => document.removeEventListener('mousedown', handleClickOutside);
+ }, []);
+
+ const handleSave = async () => {
+ await onSave(inputLevel, inputDate, selectedCharId);
+ };
+
+ const handleDelete = async (recordId: string) => {
+ if (!window.confirm("この記録を削除してもよろしいですか?")) return;
+
+ try {
+ let token = '';
+ if (typeof window !== 'undefined') {
+ const authKey = Object.keys(localStorage).find(key => key.startsWith('sb-') && key.endsWith('-auth-token'));
+ if (authKey) {
+ const authDataString = localStorage.getItem(authKey);
+ if (authDataString) {
+ const authData = JSON.parse(authDataString);
+ token = authData?.access_token || '';
+ }
+ }
+ }
+
+ const res = await fetch('/api/relationship', {
+ method: 'DELETE',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(token ? { 'Authorization': `Bearer ${token}` } : {}),
+ },
+ body: JSON.stringify({ id: recordId }),
+ });
+
+ if (!res.ok) throw new Error('Failed to delete');
+
+ window.location.reload();
+ } catch (err) {
+ console.error(err);
+ alert("削除に失敗しました。");
+ }
+ };
+
+ const renderGraph = (
+ <div
+ ref={containerRef}
+ className={`graph-container ${!isChartReady ? 'opacity-0 pointer-events-none' : ''}`}
+ data-chart-ready={isChartReady ? "true" : "false"}
+ >
+ {isChartReady && (
+ graphData.length > 0 ? (
+ <ResponsiveContainer width="100%" height="100%">
+ <AreaChart data={graphData} margin={{ top: 5, right: 5, left: -35, bottom: -10 }}>
+ <defs>
+ <linearGradient id="colorExp" x1="0" y1="0" x2="0" y2="1">
+ <stop offset="5%" stopColor="var(--primary)" stopOpacity={0.4}/>
+ <stop offset="60%" stopColor="var(--primary)" stopOpacity={0.2}/>
+ <stop offset="95%" stopColor="var(--primary)" stopOpacity={0}/>
+ </linearGradient>
+ </defs>
+ <CartesianGrid strokeDasharray="3 3" vertical={false} stroke="var(--secondary)" opacity={0.2} />
+ <XAxis
+ dataKey="timestamp"
+ type="number"
+ domain={chartDataCombined.xDomain || ['dataMin', 'dataMax']}
+ tickLine={true}
+ axisLine={true}
+ stroke="var(--secondary-foreground)"
+ fontSize={10}
+ tickCount={14}
+ tickFormatter={(ts) => {
+ try {
+ const d = new Date(ts);
+ const y = d.getFullYear();
+ const m = String(d.getMonth() + 1).padStart(2, '0');
+ return `${y}/${m}`;
+ } catch (e) {}
+ return '';
+ }}
+ />
+ <YAxis
+ dataKey="cumulative_exp"
+ domain={[0, 240225]}
+ ticks={[0, 14790, 29175, 50835, 81270, 121980, 174465, 240225]}
+ tickFormatter={(exp) => `${getLevelFromExp(exp)}`}
+ stroke="var(--secondary-foreground)"
+ fontSize={10}
+ tickLine={true}
+ axisLine={true}
+ />
+ <Tooltip
+ labelFormatter={(_, payload) => {
+ if (payload && payload[0]) {
+ const p = payload[0].payload;
+ return p.isPrediction ? `Estimated Date: ${p.recorded_at}` : `Date: ${p.recorded_at}`;
+ }
+ return '';
+ }}
+ formatter={(value: any, name: any, props: any) => {
+ const p = props.payload;
+
+ if (p.isPrediction) {
+ if (name === 'predicted_exp') {
+ return [`RANK ${p.bond_level} (Est. Target)`];
+ }
+ return null as any;
+ }
+
+ if (name === 'actual_exp') {
+ return [`RANK ${p.bond_level} (${p.actual_exp} exp)`];
+ }
+
+ return null as any;
+ }}
+ contentStyle={{
+ background: 'var(--card)',
+ border: '1px solid var(--muted)',
+ borderRadius: '0.5rem',
+ fontSize: '0.85rem',
+ }}
+ labelStyle={{
+ color: 'var(--foreground)',
+ fontWeight: 700
+ }}
+ />
+ <Area
+ type="monotone"
+ dataKey="actual_exp"
+ stroke="var(--primary)"
+ strokeWidth={3}
+ fillOpacity={1}
+ fill="url(#colorExp)"
+ connectNulls={false}
+ />
+ <Line
+ type="monotone"
+ dataKey="predicted_exp"
+ stroke="var(--primary)"
+ strokeWidth={2}
+ strokeDasharray="5 5"
+ dot={false}
+ activeDot={false}
+ connectNulls={true}
+ />
+ </AreaChart>
+ </ResponsiveContainer>
+ ) : (
+ <div className="flex h-full w-full items-center justify-center border border-dashed border-(--muted) rounded-2xl bg-(--card)/30">
+ <span className="text-lg font-bold text-(--muted-foreground) tracking-wider opacity-60">
+ No Data
+ </span>
+ </div>
+ )
+ )}
+ </div>
+ );
+
+ return (
+ <div className="bond-container">
+ <div className="bond-split-layout">
+ <div className="bond-left-panel">
+ <div className="char-selector" ref={dropdownRef}>
+ <button
+ className="char-selector-trigger"
+ onClick={() => setIsSelectOpen(!isSelectOpen)}
+ >
+ <span className="char-selector-label">Student</span>
+ <h1 className="char-selector-name">
+ {selectedChar.name}
+ <span className={`char-selector-arrow ${isSelectOpen ? 'open' : ''}`}>
+ ▼
+ </span>
+ </h1>
+ </button>
+
+ {isSelectOpen && (
+ <div className="char-dropdown scrollbar-hide animate-in fade-in zoom-in-95 duration-200">
+ <div className="char-dropdown-container">
+ {CHARACTER_LIST.map((char) => (
+ <button
+ key={char.id}
+ onClick={() => {
+ setSelectedCharId(char.id as CharacterId);
+ setIsSelectOpen(false);
+ }}
+ className={`char-dropdown-item ${selectedCharId === char.id ? 'active' : ''}`}
+ >
+ {char.name}
+ </button>
+ ))}
+ </div>
+ </div>
+ )}
+ </div>
+
+ <div className="record-input-area">
+ <div className="input-group-container">
+ <div className="input-field-wrapper">
+ <label className="input-label">Rank</label>
+ <div className="input-content">
+ <span className="rank-value">{inputLevel}</span>
+ <div className="spin-buttons-box">
+ <button
+ onClick={() => setInputLevel(prev => Math.min(100, prev + 1))}
+ className="spin-btn up"
+ >
+ ▲
+ </button>
+ <button
+ onClick={() => setInputLevel(prev => Math.max(1, prev - 1))}
+ className="spin-btn down"
+ >
+ ▼
+ </button>
+ </div>
+ </div>
+ </div>
+
+ <div className="input-field-wrapper">
+ <label className="input-label">Date</label>
+ <div className="input-content">
+ <input
+ type="date"
+ value={inputDate}
+ onChange={(e) => setInputDate(e.target.value)}
+ className="date-input-display"
+ />
+ </div>
+ </div>
+ </div>
+
+ <button
+ onClick={handleSave}
+ disabled={isSyncing}
+ className="btn-bond-record"
+ >
+ <span>Record</span>
+ <div>+</div>
+ </button>
+ </div>
+
+ <div className="section-wrapper">
+ <h3 className="gift-title">Favorite Gifts</h3>
+ <div className="gift-scroll-container scrollbar-hide">
+ {sortedGifts.map((gift) => {
+ const rating = selectedChar.giftRatings[gift.name];
+ const rankChar = rating?.slice(-1).toUpperCase();
+ const row = Math.floor(gift.spriteIdx / SPRITE_CONFIG.cols);
+ const col = gift.spriteIdx % SPRITE_CONFIG.cols;
+
+ return (
+ <div key={gift.name} className="gift-item">
+ <div className="gift-image-box">
+ <div
+ className="gift-sprite"
+ style={{ '--col': col, '--row': row } as React.CSSProperties}
+ />
+ <div className={`gift-rank-dot ${
+ rankChar === 'S' ? 's' : rankChar === 'A' ? 'a' : 'b'
+ }`} />
+ </div>
+ <span className="gift-category-text">
+ {gift.category || "GIFT"}
+ </span>
+ </div>
+ );
+ })}
+ </div>
+ </div>
+
+ <div className="section-wrapper">
+ <h3 className="section-toggle-btn" style={{ cursor: 'default' }}>
+ <span>History Log</span>
+ </h3>
+ <div className="history-grid-container">
+ {displayHistoryRows.map((record, index) => (
+ <div key={record ? record.id : `empty-${index}`} className="history-item">
+ <div className="history-log-row">
+ {record ? (
+ <>
+ <span className="history-rank-num">
+ {record.bond_level}
+ </span>
+ <span className="history-divider">|</span>
+ <span className="history-date-text">
+ {record.recorded_at}
+ </span>
+ </>
+ ) : (
+ <span className="history-empty-text">
+ <span className="history-rank-num">
+ ---
+ </span>
+ <span className="history-divider">|</span>
+ <span className="history-date-text">
+ ---- -- --
+ </span>
+ </span>
+ )}
+ </div>
+
+ {record ? (
+ <button
+ onClick={() => handleDelete(record.id)}
+ className="edit-icon-btn text-rose-500 hover:text-rose-700 hover:bg-rose-500/10"
+ aria-label="Delete record"
+ >
+ <TrashIcon />
+ </button>
+ ) : (
+ <span className="edit-btn-placeholder" />
+ )}
+ </div>
+ ))}
+ </div>
+ </div>
+
+ {!isDesktop && (
+ <div className="section-wrapper">
+ <h3 className="section-toggle-btn" style={{ cursor: 'default' }}>
+ <span>Progress Graph</span>
+ </h3>
+ <div className="animate-in fade-in duration-200">
+ {renderGraph}
+ </div>
+ </div>
+ )}
+ </div>
+ {isDesktop && (
+ <div className="bond-right-panel animate-in fade-in duration-300">
+ <h3 className="section-toggle-btn" style={{ cursor: 'default' }}>Progress Graph</h3>
+ {renderGraph}
+ </div>
+ )}
+ </div>
+ </div>
+ );
+}
+
+if (typeof window !== "undefined") {
+ const filterWarning = (args: any[], originalFn: (...args: any[]) => void) => {
+ if (
+ typeof args[0] === "string" &&
+ args[0].includes("should be greater than 0")
+ ) {
+ return;
+ }
+ originalFn(...args);
+ };
+
+ const originalConsoleError = console.error;
+ console.error = (...args: any[]) => filterWarning(args, originalConsoleError);
+ const originalConsoleWarn = console.warn;
+ console.warn = (...args: any[]) => filterWarning(args, originalConsoleWarn);
+}+
\ No newline at end of file
diff --git a/src/components/BottomNavBar.tsx b/src/components/BottomNavBar.tsx
@@ -1,7 +1,7 @@
-import React from 'react';
-import { Timer, CalendarDays } from 'lucide-react';
+import React, { useState, useEffect } from 'react';
+import { Timer, Heart } from 'lucide-react';
-type Tab = 'timer' | 'history';
+type Tab = 'timer' | 'history' | 'bond';
interface BottomNavBarProps {
activeTab: Tab;
@@ -9,30 +9,93 @@ interface BottomNavBarProps {
}
const BottomNavBar: React.FC<BottomNavBarProps> = ({ activeTab, setActiveTab }) => {
- const tabs = [
- { id: 'timer' as const, icon: Timer },
- { id: 'history' as const, icon: CalendarDays },
- ];
+ const [isDesktop, setIsDesktop] = useState(false);
+
+ useEffect(() => {
+ const handleResize = () => {
+ setIsDesktop(window.innerWidth >= 1000);
+ };
+ handleResize();
+ window.addEventListener('resize', handleResize);
+ return () => window.removeEventListener('resize', handleResize);
+ }, []);
return (
- <div className="fixed bottom-0 left-0 right-0 desktop-hidden">
- {tabs.map((tab) => {
- const Icon = tab.icon;
- const isActive = activeTab === tab.id;
-
- return (
+ <div className="fixed bottom-0 left-0 right-0 z-40 flex border-t border-muted/10 bg-background">
+ {isDesktop ? (
+ /* ==========================================
+ PC用レイアウト: 2タブ構成
+ ========================================== */
+ <>
+ <button
+ onClick={() => setActiveTab('timer')}
+ className={`btn-nav relative flex flex-col items-center justify-center ${
+ activeTab === 'timer' || activeTab === 'history' ? 'active' : ''
+ }`}
+ >
+ <div className="relative flex flex-col items-center">
+ <Timer size={30} />
+ <div className={`btn-nav-bar absolute rounded-full ${
+ activeTab === 'timer' || activeTab === 'history' ? 'active' : ''
+ }`}/>
+ </div>
+ </button>
+
+ <button
+ onClick={() => setActiveTab('bond')}
+ className={`btn-nav relative flex flex-col items-center justify-center ${
+ activeTab === 'bond' ? 'active' : ''
+ }`}
+ >
+ <div className="relative flex flex-col items-center">
+ <Heart size={30} />
+ <div className={`btn-nav-bar absolute rounded-full ${
+ activeTab === 'bond' ? 'active' : ''
+ }`}/>
+ </div>
+ </button>
+ </>
+ ) : (
+ /* ==========================================
+ スマホ用レイアウト: 3タブ構成
+ ========================================== */
+ <>
+ <button
+ onClick={() => setActiveTab('timer')}
+ className={`btn-nav relative flex flex-col items-center justify-center ${activeTab === 'timer' ? 'active' : ''}`}
+ >
+ <div className="relative flex flex-col items-center">
+ <Timer size={30} />
+ <div className={`btn-nav-bar absolute rounded-full ${activeTab === 'timer' ? 'active' : ''}`}/>
+ </div>
+ </button>
+
+ <button
+ onClick={() => setActiveTab('history')}
+ className={`btn-nav relative flex flex-col items-center justify-center ${activeTab === 'history' ? 'active' : ''}`}
+ >
+ <div className="relative flex flex-col items-center">
+ <svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
+ <rect width="18" height="18" x="3" y="4" rx="2" ry="2"/>
+ <line x1="16" x2="16" y1="2" y2="6"/>
+ <line x1="8" x2="8" y1="2" y2="6"/>
+ <line x1="3" x2="21" y1="10" y2="10"/>
+ </svg>
+ <div className={`btn-nav-bar absolute rounded-full ${activeTab === 'history' ? 'active' : ''}`}/>
+ </div>
+ </button>
+
<button
- key={tab.id}
- onClick={() => setActiveTab(tab.id)}
- className={`btn-nav relative flex flex-col items-center justify-center ${isActive ? 'active' : ''}`}
+ onClick={() => setActiveTab('bond')}
+ className={`btn-nav relative flex flex-col items-center justify-center ${activeTab === 'bond' ? 'active' : ''}`}
>
<div className="relative flex flex-col items-center">
- <Icon size={30} />
- <div className={`btn-nav-bar absolute rounded-full ${isActive ? 'active' : ''}`}/>
+ <Heart size={30} />
+ <div className={`btn-nav-bar absolute rounded-full ${activeTab === 'bond' ? 'active' : ''}`}/>
</div>
</button>
- );
- })}
+ </>
+ )}
</div>
);
};
diff --git a/src/components/NewFeatureDialog.tsx b/src/components/NewFeatureDialog.tsx
@@ -0,0 +1,81 @@
+"use client";
+
+import React, { useState, useEffect } from 'react';
+
+export default function NewFeatureDialog() {
+ const [isOpen, setIsOpen] = useState(false);
+ const [isChecked, setIsChecked] = useState(false);
+
+ useEffect(() => {
+ if (typeof window !== 'undefined') {
+ const isNotified = localStorage.getItem('new_feature_notified');
+ if (!isNotified) {
+ setIsOpen(true);
+ }
+ }
+ }, []);
+
+ const handleClose = () => {
+ if (isChecked) {
+ localStorage.setItem('new_feature_notified', 'true');
+ }
+ setIsOpen(false);
+ };
+
+ if (!isOpen) return null;
+
+ return (
+ <div className="fixed inset-0 bg-black/40 dark:bg-black/60 z-100 flex justify-center items-center p-4 animate-in fade-in duration-200 backdrop-blur-sm">
+ <div className="w-full max-w-sm rounded-2xl shadow-xl border border-dashed bg-(--card) border-(--muted) flex flex-col overflow-hidden animate-in zoom-in-95 duration-200">
+
+ <div className="border-b border-dashed flex justify-between items-center p-5">
+ <h2 className="text-xl font-bold text-(--foreground) tracking-wide flex items-center gap-2">
+ <span>✨</span> 新機能追加のお知らせ
+ </h2>
+ </div>
+
+ <div className="p-6 flex flex-col gap-4 text-sm font-medium leading-relaxed text-(--secondary-foreground)">
+ <p>いつもご利用いただきありがとうございます!</p>
+
+ <div className="bg-(--background) p-4 rounded-xl border border-dashed border-(--muted)/60 flex flex-col gap-3">
+ <div className="flex items-start gap-2">
+ <span className="text-base">▶</span>
+ <div>
+ <strong className="text-(--foreground) block font-bold">絆上げ記録機能</strong>
+ <span className="text-xs">生徒さんごとに絆上げの記録をつけられるようになりました。日付とランクを入力して登録できます。</span>
+ </div>
+ </div>
+ <div className="flex items-start gap-2">
+ <span className="text-base">▶</span>
+ <div>
+ <strong className="text-(--foreground) block font-bold">絆ランク100の未来予想線</strong>
+ <span className="text-xs">現在の進捗ペースから絆ランク100に到達する日付を予測して、グラフに破線で描画します。</span>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ {/* 下部アクションエリア(チェックボックスとボタン) */}
+ <div className="p-4 bg-(--background)/50 border-t border-dashed border-(--muted)/40 flex flex-col gap-3">
+ <label className="flex items-center gap-2 px-1 cursor-pointer select-none text-xs text-(--secondary-foreground) font-bold">
+ <input
+ type="checkbox"
+ checked={isChecked}
+ onChange={(e) => setIsChecked(e.target.checked)}
+ className="w-4 h-4 rounded border-(--muted) text-(--primary) focus:ring-(--primary) cursor-pointer"
+ />
+ <span>今後このお知らせを表示しない</span>
+ </label>
+
+ <button
+ onClick={handleClose}
+ className="w-full py-3 rounded-xl font-bold bg-(--primary) text-(--primary-foreground) transition-all active:scale-98 cursor-pointer shadow-sm hover:brightness-105"
+ >
+ 閉じる
+ </button>
+ </div>
+
+ </div>
+ </div>
+ );
+}+
\ No newline at end of file
diff --git a/src/components/Settings.tsx b/src/components/Settings.tsx
@@ -40,7 +40,7 @@ const Settings = ({ onOpenContent }: SettingsProps) => {
{ label: 'FAQ', key: 'faq' },
{ label: '利用規約', key: 'terms' },
{ label: 'プライバシーポリシー', key: 'privacy' },
- { label: '運営者情報・変更履歴', key: 'operator' },
+ { label: '運営者情報など', key: 'operator' },
];
const handleNotificationClick = async () => {
diff --git a/src/components/TimerDashboard.tsx b/src/components/TimerDashboard.tsx
@@ -124,7 +124,7 @@ export default function TimerDashboard({
return (
<div className="dashboard-container">
<div className="timer-card">
- <div className="flex items-center justify-between mb-4">
+ <div className="flex items-center justify-between mb-2">
<h2 className="timer-card-title">Next Tap</h2>
</div>
@@ -174,7 +174,7 @@ export default function TimerDashboard({
<h2 className="timer-card-title mb-4">Next Call</h2>
<div className="grid grid-cols-2 gap-4">
<div className="flex flex-col gap-2">
- <div className="countdown-text-s bg-background/50 p-2 rounded">
+ <div className="countdown-text-s bg-background/50 pt-1 pl-1 rounded">
<CountdownDisplay milliseconds={ticket1Remaining} />
</div>
<button
@@ -184,7 +184,7 @@ export default function TimerDashboard({
><span>{ isSyncing ? 'Wait...' : 'Ticket 1' }</span></button>
</div>
<div className="flex flex-col gap-2">
- <div className="countdown-text-s bg-background/50 p-2 rounded">
+ <div className="countdown-text-s bg-background/50 pt-1 pl-1 rounded">
<CountdownDisplay milliseconds={ticket2Remaining} />
</div>
<button
diff --git a/src/components/pages.tsx b/src/components/pages.tsx
@@ -17,6 +17,7 @@ export const OVERLAY_CONTENTS: Record<string, { title: string; body: React.React
<li>生徒さんの訪問タイミング(4:00/16:00 JST)のカウントダウン</li>
<li>招待券のクールタイム管理</li>
<li>タップ履歴のカレンダー表示</li>
+ <li>絆上げの記録とグラフ表示</li>
</ul>
<h2 className="text-xl font-bold mt-8 mb-4 text-foreground">対応プラットフォーム</h2>
<p>
@@ -73,7 +74,7 @@ export const OVERLAY_CONTENTS: Record<string, { title: string; body: React.React
<div className="bg-blue-500/10 border border-blue-500/50 rounded-lg p-4 text-sm mb-4">
<p className="font-bold mb-2">ℹ️ お願い</p>
<p>
- 通知登録されている端末数が増加すると、運営者の大人のカードの出番がやってきてしまいます。
+ 通知登録されている端末数が増加すると、運営者の大人のカードの出番がやってくるかもしれません。
</p>
<p className="mt-1">
通知登録は1ユーザーあたり2端末までを目安とし、通知が不要な端末は都度登録解除していただくよう、何卒ご協力をお願いいたします。
@@ -103,7 +104,7 @@ export const OVERLAY_CONTENTS: Record<string, { title: string; body: React.React
<h3 className="font-bold text-foreground">
Q: 記録を間違えてタップしてしまいました。削除できますか?</h3>
<p className="font-normal text-sm">
- A: Tap履歴を個別に編集・削除することはできません。データベースへの接続増加を伴うため、現時点では実装予定はありません。</p>
+ A: カフェタッチのTap履歴は個別に編集・削除することはできません。絆ランク記録は直近10件のデータまで削除可能です。</p>
</div>
<div className="flex flex-col gap-1">
@@ -141,14 +142,14 @@ export const OVERLAY_CONTENTS: Record<string, { title: string; body: React.React
<h3 className="font-bold text-foreground">
Q: オフラインでも利用できますか?</h3>
<p className="font-normal text-sm">
- A: 利用できません。インターネットに接続が必要です。</p>
+ A: 利用できません。インターネットへの接続が必要です。</p>
</div>
<div className="flex flex-col gap-1">
<h3 className="font-bold text-foreground">
Q: ゲーム内部のデータを使用していますか?</h3>
<p className="font-normal text-sm">
- A: 本サイトはゲーム「ブルーアーカイブ」内のいかなるアセットも使用していません。また、ゲームデータにもアクセスしていませんので、本サイトがゲーム内のアカウントに影響を与えることはありません。</p>
+ A: 本サイトはゲーム「ブルーアーカイブ」内に実装されている機能などは使用していません。また、ゲームデータ自体にもアクセスしていませんので、本サイトがゲーム内のアカウントに影響を与えることはありません。</p>
</div>
<div className="flex flex-col gap-1">
@@ -280,9 +281,10 @@ export const OVERLAY_CONTENTS: Record<string, { title: string; body: React.React
),
},
operator: {
- title: "運営者情報・変更履歴",
+ title: "運営者情報など",
body: (() => {
const updateLogs = [
+ { date: '2026-06-07', content: '絆ランク記録機能追加' },
{ date: '2026-02-08', content: '初回リリース' },
];
@@ -302,24 +304,28 @@ export const OVERLAY_CONTENTS: Record<string, { title: string; body: React.React
ハチか<span className="ml-2 mr-1">X:</span><a href="https://x.com/bite_sour_sweet" target="_blank" rel="noopener noreferrer" className="hover:underline text-blue-400">@bite_sour_sweet</a>
</p>
</div>
+ <div className="flex flex-col gap-2">
+ <h3 className="font-bold text-foreground border-b border-muted pb-1">謝辞</h3>
+ <p className="font-normal">
+ ブルーアーカイブ(ブルアカ)攻略 Wiki 様(絆ランクテーブル・生徒さんごとの贈り物対応付けに使用)<span className="ml-2 mr-1">HP:</span><a href="https://bluearchive.wikiru.jp/" target="_blank" rel="noopener noreferrer" className="hover:underline text-blue-400">https://bluearchive.wikiru.jp/</a>
+ </p>
+ </div>
</div>
-
- <div className="flex flex-col gap-1 pb-2">
+ <div className="flex flex-col gap-2">
<h3 className="font-bold text-foreground border-b border-muted pb-1">変更履歴</h3>
<div className="bg-background/50 rounded-md p-1.5 max-h-48 overflow-y-auto space-y-2 text-sm">
{updateLogs.map((log, index) => (
- <div key={index} className="flex gap-3 border-b border-muted/30 pb-1.5 last:border-0">
- <span className="font-mono shrink-0">{log.date}</span>
+ <div key={index} className="flex gap-3">
+ <span className="shrink-0">{log.date}</span>
<p className="text-foreground/80">{log.content}</p>
</div>
))}
</div>
</div>
-
<div className="flex flex-col gap-2">
<h3 className="font-bold text-foreground border-b border-muted pb-1">連絡先・フィードバック</h3>
<p className="text-sm leading-relaxed">
- 不具合報告やご要望は、
+ 不具合・ミスのご報告やご要望は、
<a href="https://github.com/Sunny-JP/ba-cafe" target="_blank" rel="noopener noreferrer" className="mx-1 hover:underline text-blue-400">
GitHub
</a>
diff --git a/src/lib/bondData.ts b/src/lib/bondData.ts
@@ -0,0 +1,3267 @@
+// 絆レベル累計EXPテーブル
+export const BOND_EXP_TABLE: Record<number, number> = {
+ 1: 0, 2: 15, 3: 45, 4: 75, 5: 110,
+ 6: 145, 7: 180, 8: 220, 9: 260, 10: 300,
+ 11: 360, 12: 450, 13: 555, 14: 675, 15: 815,
+ 16: 975, 17: 1155, 18: 1360, 19: 1590, 20: 1845,
+ 21: 2130, 22: 2445, 23: 2790, 24: 3165, 25: 3575,
+ 26: 4020, 27: 4500, 28: 5020, 29: 5580, 30: 6180,
+ 31: 6825, 32: 7515, 33: 8250, 34: 9030, 35: 9860,
+ 36: 10740, 37: 11670, 38: 12655, 39: 13695, 40: 14790,
+ 41: 15945, 42: 17160, 43: 18435, 44: 19770, 45: 21170,
+ 46: 22635, 47: 24165, 48: 25765, 49: 27435, 50: 29175,
+ 51: 30990, 52: 32880, 53: 34845, 54: 36885, 55: 39005,
+ 56: 41205, 57: 43485, 58: 45850, 59: 48300, 60: 50835,
+ 61: 53460, 62: 56175, 63: 58980, 64: 61875, 65: 64865,
+ 66: 67950, 67: 71130, 68: 74410, 69: 77790, 70: 81270,
+ 71: 84855, 72: 88545, 73: 92340, 74: 96240, 75: 100250,
+ 76: 104370, 77: 108600, 78: 112945, 79: 117405, 80: 121980,
+ 81: 126675, 82: 131490, 83: 136425, 84: 141480, 85: 146660,
+ 86: 151965, 87: 157395, 88: 162955, 89: 168645, 90: 174465,
+ 91: 180420, 92: 186510, 93: 192735, 94: 199095, 95: 205595,
+ 96: 212235, 97: 219015, 98: 225940, 99: 233010, 100: 240225
+};
+
+export const SPRITE_CONFIG = {
+ cols: 5, // 1行あたりのコマ数
+ width: 142, // 1コマの横幅
+ height: 112, // 1コマの高さ
+};
+
+export const GIFT_EXP = {
+ nb: 1.0, // 通常 効果中
+ na: 1.5, // 通常 効果大
+ ns: 2.0, // 通常 効果特大
+ sa: 1.5, // 高級 効果大
+ ss: 2.0, // 高級 効果特大
+} as const;
+
+export const GIFT_LIST = [
+ { name: 'レースの枕', category:'煌めき', baseExp: 120, spriteIdx: 0 },
+ { name: 'ストリートオブヤンキー1巻', category:'煌めき', baseExp: 120, spriteIdx: 1 },
+ { name: 'サミュエラ「ザ・ビヨンド」', category:'煌めき', baseExp: 120, spriteIdx: 2 },
+ { name: 'レトロな卵の工芸品', category:'煌めき', baseExp: 120, spriteIdx: 3 },
+ { name: 'ミルフィーユの正統派パフェ', category:'煌めき', baseExp: 120, spriteIdx: 4 },
+ { name: 'エイ~ブックレア', category:'煌めき', baseExp: 120, spriteIdx: 5 },
+ { name: 'おしゃれなくし', category:'煌めき', baseExp: 120, spriteIdx: 6 },
+ { name: '栄養満載の総合ビタミンゼリー', category:'煌めき', baseExp: 120, spriteIdx: 7 },
+ { name: '最高級の楓の盆栽', category:'煌めき', baseExp: 120, spriteIdx: 8 },
+ { name: '裁縫キット', category:'煌めき', baseExp: 120, spriteIdx: 9 },
+ { name: '音楽演奏会入場券', category:'煌めき', baseExp: 120, spriteIdx: 10 },
+ { name: '1日3回ダンベルセット', category:'煌めき', baseExp: 120, spriteIdx: 11 },
+ { name: 'ボードゲーム「ザ・人生」', category:'煌めき',baseExp: 120, spriteIdx: 12 },
+
+ { name: 'ウェーブキャットの枕', category:'牡丹', baseExp: 40, spriteIdx: 20 },
+ { name: 'ペロロの腹筋ローラー', category:'アサガオ', baseExp: 40, spriteIdx: 21 },
+ { name: 'エーポッドプロ', category:'薔薇', baseExp: 40, spriteIdx: 22 },
+ { name: '禁断の愛~許されないからこそ美しく~', category:'チューリップ', baseExp: 40, spriteIdx: 23 },
+ { name: 'ゲームマガジン「ヒットガールズ」', category:'チューリップ', baseExp: 40, spriteIdx: 24 },
+ { name: 'チェリーローズカラーのグロス', category:'桜', baseExp: 40, spriteIdx: 25 },
+ { name: 'お肌を透明にするBBクリーム', category:'桜', baseExp: 40, spriteIdx: 26 },
+ { name: 'ミリタリー用カモフラージュクリーム3種セット', category:'桜', baseExp: 40, spriteIdx: 27 },
+ { name: '高級なクッキーセット', category:'スイセン', baseExp: 40, spriteIdx: 28 },
+ { name: '『銃 可愛い 青春』', category:'チューリップ', baseExp: 40, spriteIdx: 29 },
+ { name: '天体望遠鏡', category:'百日紅', baseExp: 40, spriteIdx: 30 },
+ { name: 'ゲームガールカラー復刻版', category:'薔薇', baseExp: 40, spriteIdx: 31 },
+ { name: 'MX-レーションC型デザート風味', category:'スイセン', baseExp: 40, spriteIdx: 32 },
+ { name: '抹茶味の瓶ラムネ', category:'スイセン', baseExp: 40, spriteIdx: 33 },
+ { name: 'ゼリーズの枕', category:'牡丹', baseExp: 40, spriteIdx: 34 },
+ { name: '跳躍探偵ウサギ~霧に包まれた温泉での滑落~', category:'チューリップ', baseExp: 40, spriteIdx: 35 },
+ { name: 'O-フィット', category:'アサガオ', baseExp: 40, spriteIdx: 36 },
+ { name: '埋蔵金の地図', category:'タンポポ', baseExp: 40, spriteIdx: 37 },
+ { name: 'コスプレ用ぐるぐるメガネ', category:'百日紅', baseExp: 40, spriteIdx: 38 },
+ { name: '世界で最も無駄な絡繰りボックス', category:'タンポポ', baseExp: 40, spriteIdx: 39 },
+ { name: 'リボンのついた熊のぬいぐるみ', category:'百日紅', baseExp: 40, spriteIdx: 40 },
+ { name: '映画の前売りペアチケット', category:'ユリ', baseExp: 40, spriteIdx: 41 },
+ { name: '30色の絵の具セット', category:'百日紅', baseExp: 40, spriteIdx: 42 },
+ { name: '古典の詩集', category:'チューリップ', baseExp: 40, spriteIdx: 43 },
+ { name: '可愛い食器セット', category:'キンセンカ', baseExp: 40, spriteIdx: 44 },
+ { name: '頭脳開発キューブパズル', category:'百日紅', baseExp: 40, spriteIdx: 45 },
+ { name: '大きなホールケーキ', category:'スイセン', baseExp: 40, spriteIdx: 46 },
+ { name: 'ハイクラスビュッフェ招待券', category:'ユリ', baseExp: 40, spriteIdx: 47 },
+ { name: '食虫植物の植木鉢', category:'桔梗', baseExp: 40, spriteIdx: 48 },
+ { name: '高級そうな欲望のつぼ', category:'タンポポ', baseExp: 40, spriteIdx: 49 },
+ { name: 'ぜんまい式オルゴール', category:'タンポポ', baseExp: 40, spriteIdx: 50 },
+ { name: '刺繍付きのハンカチ', category:'モクレン', baseExp: 40, spriteIdx: 51 },
+ { name: '百科事典', category:'チューリップ', baseExp: 40, spriteIdx: 52 },
+ { name: 'ザ・サプリメント', category:'ツツジ', baseExp: 40, spriteIdx: 53 },
+ { name: '夏模様の浮き輪', category:'翡翠花', baseExp: 40, spriteIdx: 54 }
+] as const;
+
+export const CHARACTER_LIST = [
+ {
+ id: 'airi',
+ name: 'アイリ',
+ giftRatings: {
+ 'ミルフィーユの正統派パフェ': 'sa',
+ '高級なクッキーセット': 'na',
+ 'MX-レーションC型デザート風味': 'nb',
+ '抹茶味の瓶ラムネ': 'nb',
+ '大きなホールケーキ': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'airi_band',
+ name: 'アイリ(バンド)',
+ giftRatings: {
+ 'ミルフィーユの正統派パフェ': 'ss',
+ '高級なクッキーセット': 'na',
+ '大きなホールケーキ': 'na',
+ 'MX-レーションC型デザート風味': 'nb',
+ '抹茶味の瓶ラムネ': 'nb',
+ '刺繍付きのハンカチ': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'aoba',
+ name: 'アオバ',
+ giftRatings: {
+ '裁縫キット': 'ss',
+ '栄養満載の総合ビタミンゼリー': 'sa',
+ '音楽演奏会入場券': 'sa',
+ 'ハイクラスビュッフェ招待券': 'na',
+ '映画の前売りペアチケット': 'nb',
+ '可愛い食器セット': 'nb',
+ 'ザ・サプリメント': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'akane',
+ name: 'アカネ',
+ giftRatings: {
+ '裁縫キット': 'sa',
+ '可愛い食器セット': 'na',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'akane_bunnygirl',
+ name: 'アカネ(バニーガール)',
+ giftRatings: {
+ '裁縫キット': 'ss',
+ '可愛い食器セット': 'na',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'akari',
+ name: 'アカリ',
+ giftRatings: {
+ 'ミルフィーユの正統派パフェ': 'ss',
+ 'ハイクラスビュッフェ招待券': 'na',
+ '大きなホールケーキ': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'akari_newyear',
+ name: 'アカリ(正月)',
+ giftRatings: {
+ 'ミルフィーユの正統派パフェ': 'ss',
+ 'ハイクラスビュッフェ招待券': 'na',
+ '大きなホールケーキ': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'ako',
+ name: 'アコ',
+ giftRatings: {
+ 'レトロな卵の工芸品': 'sa',
+ 'ザ・サプリメント': 'na',
+ '埋蔵金の地図': 'nb',
+ '世界で最も無駄な絡繰りボックス': 'nb',
+ '高級そうな欲望のつぼ': 'nb',
+ 'ぜんまい式オルゴール': 'nb',
+ '百科事典': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'ako_dress',
+ name: 'アコ(ドレス)',
+ giftRatings: {
+ '栄養満載の総合ビタミンゼリー': 'ss',
+ 'レトロな卵の工芸品': 'sa',
+ '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'na',
+ 'ザ・サプリメント': 'na',
+ '埋蔵金の地図': 'nb',
+ '世界で最も無駄な絡繰りボックス': 'nb',
+ '高級そうな欲望のつぼ': 'nb',
+ 'ぜんまい式オルゴール': 'nb',
+ '百科事典': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'azusa',
+ name: 'アズサ',
+ giftRatings: {
+ 'おしゃれなくし': 'sa',
+ 'リボンのついた熊のぬいぐるみ': 'na',
+ 'ウェーブキャットの枕': 'nb',
+ 'ペロロの腹筋ローラー': 'nb',
+ '刺繍付きのハンカチ': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'azusa_swimsuit',
+ name: 'アズサ(水着)',
+ giftRatings: {
+ 'おしゃれなくし': 'ss',
+ 'リボンのついた熊のぬいぐるみ': 'na',
+ '夏模様の浮き輪': 'na',
+ 'ウェーブキャットの枕': 'nb',
+ 'ペロロの腹筋ローラー': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'asuna',
+ name: 'アスナ',
+ giftRatings: {
+ 'ミルフィーユの正統派パフェ': 'ss',
+ 'サミュエラ「ザ・ビヨンド」': 'sa',
+ 'チェリーローズカラーのグロス': 'na',
+ 'お肌を透明にするBBクリーム': 'nb',
+ 'ミリタリー用カモフラージュクリーム3種セット': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'asuna_bunnygirl',
+ name: 'アスナ(バニーガール)',
+ giftRatings: {
+ 'サミュエラ「ザ・ビヨンド」': 'ss',
+ 'ミルフィーユの正統派パフェ': 'sa',
+ '大きなホールケーキ': 'na',
+ '高級なクッキーセット': 'nb',
+ 'MX-レーションC型デザート風味': 'nb',
+ '抹茶味の瓶ラムネ': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'asuna_schooluniform',
+ name: 'アスナ(制服)',
+ giftRatings: {
+ 'サミュエラ「ザ・ビヨンド」': 'ss',
+ 'チェリーローズカラーのグロス': 'na',
+ 'お肌を透明にするBBクリーム': 'na',
+ 'ミリタリー用カモフラージュクリーム3種セット': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'atsuko',
+ name: 'アツコ',
+ giftRatings: {
+ 'ボードゲーム「ザ・人生」': 'ss',
+ '百科事典': 'na',
+ 'ザ・サプリメント': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'atsuko_swimsuit',
+ name: 'アツコ(水着)',
+ giftRatings: {
+ 'ボードゲーム「ザ・人生」': 'ss',
+ 'ストリートオブヤンキー1巻': 'sa',
+ '古典の詩集': 'na',
+ '百科事典': 'na',
+ '夏模様の浮き輪': 'na',
+ '禁断の愛~許されないからこそ美しく~': 'nb',
+ 'ゲームマガジン「ヒットガールズ」': 'nb',
+ '『銃 可愛い 青春』': 'nb',
+ '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'nb',
+ 'コスプレ用ぐるぐるメガネ': 'nb',
+ '食虫植物の植木鉢': 'nb',
+ '高級そうな欲望のつぼ': 'nb',
+ 'ザ・サプリメント': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'ayane',
+ name: 'アヤネ',
+ giftRatings: {
+ 'ストリートオブヤンキー1巻': 'sa',
+ '『銃 可愛い 青春』': 'na',
+ '禁断の愛~許されないからこそ美しく~': 'nb',
+ 'ゲームマガジン「ヒットガールズ」': 'nb',
+ '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'nb',
+ '古典の詩集': 'nb',
+ '百科事典': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'ayane_swimsuit',
+ name: 'アヤネ(水着)',
+ giftRatings: {
+ 'エイ~ブックレア': 'ss',
+ 'ストリートオブヤンキー1巻': 'sa',
+ '『銃 可愛い 青春』': 'na',
+ '夏模様の浮き輪': 'na',
+ '禁断の愛~許されないからこそ美しく~': 'nb',
+ 'ゲームマガジン「ヒットガールズ」': 'nb',
+ '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'nb',
+ '古典の詩集': 'nb',
+ '百科事典': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'arisu',
+ name: 'アリス',
+ giftRatings: {
+ 'ボードゲーム「ザ・人生」': 'ss',
+ 'ゲームマガジン「ヒットガールズ」': 'na',
+ 'ゲームガールカラー復刻版': 'nb',
+ 'O-フィット': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'arisu_maid',
+ name: 'アリス(メイド)',
+ giftRatings: {
+ 'おしゃれなくし': 'ss',
+ 'ボードゲーム「ザ・人生」': 'ss',
+ 'ゲームマガジン「ヒットガールズ」': 'na',
+ '頭脳開発キューブパズル': 'na',
+ '天体望遠鏡': 'nb',
+ 'ゲームガールカラー復刻版': 'nb',
+ 'O-フィット': 'nb',
+ 'コスプレ用ぐるぐるメガネ': 'nb',
+ 'リボンのついた熊のぬいぐるみ': 'nb',
+ '30色の絵の具セット': 'nb',
+ '刺繍付きのハンカチ': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'arisu_battle',
+ name: 'アリス(臨戦)',
+ giftRatings: {
+ 'ボードゲーム「ザ・人生」': 'ss',
+ 'おしゃれなくし': 'sa',
+ 'ゲームガールカラー復刻版': 'na',
+ 'ゲームマガジン「ヒットガールズ」': 'nb',
+ '天体望遠鏡': 'nb',
+ 'O-フィット': 'nb',
+ 'コスプレ用ぐるぐるメガネ': 'nb',
+ 'リボンのついた熊のぬいぐるみ': 'nb',
+ '30色の絵の具セット': 'nb',
+ '頭脳開発キューブパズル': 'nb',
+ '刺繍付きのハンカチ': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'aru',
+ name: 'アル',
+ giftRatings: {
+ 'レトロな卵の工芸品': 'sa',
+ '埋蔵金の地図': 'na',
+ '世界で最も無駄な絡繰りボックス': 'nb',
+ '高級そうな欲望のつぼ': 'nb',
+ 'ぜんまい式オルゴール': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'aru_newyear',
+ name: 'アル(正月)',
+ giftRatings: {
+ 'レトロな卵の工芸品': 'sa',
+ '埋蔵金の地図': 'na',
+ '世界で最も無駄な絡繰りボックス': 'nb',
+ '高級そうな欲望のつぼ': 'nb',
+ 'ぜんまい式オルゴール': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'aru_dress',
+ name: 'アル(ドレス)',
+ giftRatings: {
+ 'レトロな卵の工芸品': 'ss',
+ '埋蔵金の地図': 'na',
+ '世界で最も無駄な絡繰りボックス': 'na',
+ '高級そうな欲望のつぼ': 'na',
+ 'ぜんまい式オルゴール': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'iori',
+ name: 'イオリ',
+ giftRatings: {
+ 'ミルフィーユの正統派パフェ': 'sa',
+ 'MX-レーションC型デザート風味': 'na',
+ 'ミリタリー用カモフラージュクリーム3種セット': 'nb',
+ '高級なクッキーセット': 'nb',
+ '抹茶味の瓶ラムネ': 'nb',
+ '大きなホールケーキ': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'iori_swimsuit',
+ name: 'イオリ(水着)',
+ giftRatings: {
+ 'ミルフィーユの正統派パフェ': 'sa',
+ 'MX-レーションC型デザート風味': 'na',
+ '夏模様の浮き輪': 'na',
+ 'ミリタリー用カモフラージュクリーム3種セット': 'nb',
+ '高級なクッキーセット': 'nb',
+ '抹茶味の瓶ラムネ': 'nb',
+ '大きなホールケーキ': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'izuna',
+ name: 'イズナ',
+ giftRatings: {
+ 'おしゃれなくし': 'sa',
+ '刺繍付きのハンカチ': 'na',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'izuna_swimsuit',
+ name: 'イズナ(水着)',
+ giftRatings: {
+ 'おしゃれなくし': 'sa',
+ '刺繍付きのハンカチ': 'na',
+ '夏模様の浮き輪': 'na',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'izumi',
+ name: 'イズミ',
+ giftRatings: {
+ 'ミルフィーユの正統派パフェ': 'sa',
+ '抹茶味の瓶ラムネ': 'na',
+ '高級なクッキーセット': 'nb',
+ 'MX-レーションC型デザート風味': 'nb',
+ '大きなホールケーキ': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'izumi_swimsuit',
+ name: 'イズミ(水着)',
+ giftRatings: {
+ 'ミルフィーユの正統派パフェ': 'sa',
+ '抹茶味の瓶ラムネ': 'na',
+ '夏模様の浮き輪': 'na',
+ '高級なクッキーセット': 'nb',
+ 'MX-レーションC型デザート風味': 'nb',
+ '大きなホールケーキ': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'izumi_newyear',
+ name: 'イズミ(正月)',
+ giftRatings: {
+ 'ミルフィーユの正統派パフェ': 'sa',
+ '抹茶味の瓶ラムネ': 'na',
+ '高級なクッキーセット': 'nb',
+ 'MX-レーションC型デザート風味': 'nb',
+ '大きなホールケーキ': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'ichika',
+ name: 'イチカ',
+ giftRatings: {
+ 'サミュエラ「ザ・ビヨンド」': 'ss',
+ 'エイ~ブックレア': 'sa',
+ 'おしゃれなくし': 'sa',
+ 'チェリーローズカラーのグロス': 'na',
+ 'エーポッドプロ': 'nb',
+ 'お肌を透明にするBBクリーム': 'nb',
+ 'ミリタリー用カモフラージュクリーム3種セット': 'nb',
+ 'ゲームガールカラー復刻版': 'nb',
+ '刺繍付きのハンカチ': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'ichika_swimsuit',
+ name: 'イチカ(水着)',
+ giftRatings: {
+ 'サミュエラ「ザ・ビヨンド」': 'ss',
+ 'エイ~ブックレア': 'sa',
+ 'チェリーローズカラーのグロス': 'na',
+ '夏模様の浮き輪': 'na',
+ 'エーポッドプロ': 'nb',
+ 'お肌を透明にするBBクリーム': 'nb',
+ 'ミリタリー用カモフラージュクリーム3種セット': 'nb',
+ 'ゲームガールカラー復刻版': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'ibuki',
+ name: 'イブキ',
+ giftRatings: {
+ 'ミルフィーユの正統派パフェ': 'ss',
+ 'ボードゲーム「ザ・人生」': 'ss',
+ '高級なクッキーセット': 'na',
+ 'リボンのついた熊のぬいぐるみ': 'na',
+ '30色の絵の具セット': 'na',
+ '天体望遠鏡': 'nb',
+ 'MX-レーションC型デザート風味': 'nb',
+ '抹茶味の瓶ラムネ': 'nb',
+ 'コスプレ用ぐるぐるメガネ': 'nb',
+ '頭脳開発キューブパズル': 'nb',
+ '大きなホールケーキ': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'iroha',
+ name: 'イロハ',
+ giftRatings: {
+ 'ストリートオブヤンキー1巻': 'sa',
+ 'ゲームマガジン「ヒットガールズ」': 'na',
+ '禁断の愛~許されないからこそ美しく~': 'nb',
+ '『銃 可愛い 青春』': 'nb',
+ 'ゲームガールカラー復刻版 ': 'nb',
+ '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'nb',
+ 'O-フィット': 'nb',
+ '古典の詩集': 'nb',
+ '百科事典': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'ui',
+ name: 'ウイ',
+ giftRatings: {
+ 'レトロな卵の工芸品': 'ss',
+ 'ストリートオブヤンキー1巻': 'sa',
+ '『銃 可愛い 青春』': 'na',
+ '古典の詩集': 'na',
+ '禁断の愛~許されないからこそ美しく~': 'nb',
+ 'ゲームマガジン「ヒットガールズ」': 'nb',
+ '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'nb',
+ '百科事典': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'ui_swimsuit',
+ name: 'ウイ(水着)',
+ giftRatings: {
+ 'ストリートオブヤンキー1巻': 'ss',
+ '『銃 可愛い 青春』': 'na',
+ '埋蔵金の地図': 'na',
+ '古典の詩集': 'na',
+ '夏模様の浮き輪': 'na',
+ '禁断の愛~許されないからこそ美しく~': 'nb',
+ 'ゲームマガジン「ヒットガールズ」': 'nb',
+ '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'nb',
+ '百科事典': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'utaha',
+ name: 'ウタハ',
+ giftRatings: {
+ 'エイ~ブックレア': 'ss',
+ '天体望遠鏡': 'na',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'utaha_cheerleader',
+ name: 'ウタハ(応援団)',
+ giftRatings: {
+ 'エイ~ブックレア': 'ss',
+ '天体望遠鏡': 'na',
+ 'エーポッドプロ': 'nb',
+ 'ゲームガールカラー復刻版': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'umika',
+ name: 'ウミカ',
+ giftRatings: {
+ 'ボードゲーム「ザ・人生」': 'ss',
+ 'コスプレ用ぐるぐるメガネ': 'na',
+ '天体望遠鏡': 'nb',
+ 'リボンのついた熊のぬいぐるみ': 'nb',
+ '30色の絵の具セット': 'nb',
+ '頭脳開発キューブパズル': 'nb',
+ '食虫植物の植木鉢': 'nb',
+ '高級そうな欲望のつぼ': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'eimi',
+ name: 'エイミ',
+ giftRatings: {
+ 'レトロな卵の工芸品': 'sa',
+ 'ぜんまい式オルゴール': 'na',
+ 'エーポッドプロ': 'nb',
+ '埋蔵金の地図': 'nb',
+ '世界で最も無駄な絡繰りボックス': 'nb',
+ '高級そうな欲望のつぼ': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'eimi_swimsuit',
+ name: 'エイミ(水着)',
+ giftRatings: {
+ 'レトロな卵の工芸品': 'sa',
+ 'ぜんまい式オルゴール': 'na',
+ '夏模様の浮き輪': 'na',
+ 'エーポッドプロ': 'nb',
+ '埋蔵金の地図': 'nb',
+ '世界で最も無駄な絡繰りボックス': 'nb',
+ '高級そうな欲望のつぼ': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'eimi_battle',
+ name: 'エイミ(臨戦)',
+ giftRatings: {
+ '栄養満載の総合ビタミンゼリー': 'ss',
+ '禁断の愛~許されないからこそ美しく~': 'na',
+ 'ゲームマガジン「ヒットガールズ」': 'nb',
+ 'ゲームガールカラー復刻版': 'nb',
+ 'O-フィット': 'nb',
+ '埋蔵金の地図': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'eri',
+ name: 'エリ',
+ giftRatings: {
+ 'レトロな卵の工芸品': 'ss',
+ '埋蔵金の地図': 'na',
+ '30色の絵の具セット': 'na',
+ '世界で最も無駄な絡繰りボックス': 'nb',
+ '高級そうな欲望のつぼ': 'nb',
+ 'ぜんまい式オルゴール': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'erika',
+ name: 'エリカ',
+ giftRatings: {
+ 'おしゃれなくし': 'ss',
+ 'ストリートオブヤンキー1巻': 'sa',
+ 'サミュエラ「ザ・ビヨンド」': 'sa',
+ 'リボンのついた熊のぬいぐるみ': 'na',
+ '禁断の愛~許されないからこそ美しく~': 'nb',
+ 'ゲームマガジン「ヒットガールズ」': 'nb',
+ 'チェリーローズカラーのグロス': 'nb',
+ 'お肌を透明にするBBクリーム': 'nb',
+ 'ミリタリー用カモフラージュクリーム3種セット': 'nb',
+ '『銃 可愛い 青春』': 'nb',
+ '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'nb',
+ '古典の詩集': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'otogi',
+ name: 'オトギ',
+ giftRatings: {
+ 'レトロな卵の工芸品': 'ss',
+ '1日3回ダンベルセット': 'sa',
+ 'O-フィット': 'na',
+ 'ペロロの腹筋ローラー': 'nb',
+ 'ミリタリー用カモフラージュクリーム3種セット': 'nb',
+ 'MX-レーションC型デザート風味': 'nb',
+ '埋蔵金の地図': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'kaede',
+ name: 'カエデ',
+ giftRatings: {
+ 'ボードゲーム「ザ・人生」': 'sa',
+ 'コスプレ用ぐるぐるメガネ': 'na',
+ 'ゲームマガジン「ヒットガールズ」': 'nb',
+ '天体望遠鏡': 'nb',
+ 'ゲームガールカラー復刻版': 'nb',
+ 'O-フィット': 'nb',
+ 'リボンのついた熊のぬいぐるみ': 'nb',
+ '30色の絵の具セット': 'nb',
+ '頭脳開発キューブパズル': 'nb',
+ '食虫植物の植木鉢': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'kazusa',
+ name: 'カズサ',
+ giftRatings: {
+ 'ミルフィーユの正統派パフェ': 'sa',
+ '高級なクッキーセット': 'na',
+ 'チェリーローズカラーのグロス': 'nb',
+ 'MX-レーションC型デザート風味': 'nb',
+ '抹茶味の瓶ラムネ': 'nb',
+ '大きなホールケーキ': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'kazusa_band',
+ name: 'カズサ(バンド)',
+ giftRatings: {
+ 'ミルフィーユの正統派パフェ': 'ss',
+ '高級なクッキーセット': 'na',
+ '大きなホールケーキ': 'na',
+ 'エーポッドプロ': 'nb',
+ 'MX-レーションC型デザート風味': 'nb',
+ '抹茶味の瓶ラムネ': 'nb',
+ 'ぜんまい式オルゴール': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'kasumi',
+ name: 'カスミ',
+ giftRatings: {
+ 'エイ~ブックレア': 'ss',
+ '埋蔵金の地図': 'na',
+ '天体望遠鏡': 'nb',
+ '世界で最も無駄な絡繰りボックス': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'kanoe',
+ name: 'カノエ',
+ giftRatings: {
+ 'サミュエラ「ザ・ビヨンド」': 'ss',
+ 'ストリートオブヤンキー1巻': 'sa',
+ 'エーポッドプロ': 'na',
+ '禁断の愛~許されないからこそ美しく~': 'na',
+ 'ゲームマガジン「ヒットガールズ」': 'nb',
+ '『銃 可愛い 青春』': 'nb',
+ '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'nb',
+ '古典の詩集': 'nb',
+ '百科事典': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'kaho',
+ name: 'カホ',
+ giftRatings: {
+ 'ストリートオブヤンキー1巻': 'ss',
+ '古典の詩集': 'na',
+ '禁断の愛~許されないからこそ美しく~': 'nb',
+ 'ゲームマガジン「ヒットガールズ」': 'nb',
+ '『銃 可愛い 青春』': 'nb',
+ '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'nb',
+ '百科事典': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'kayoko',
+ name: 'カヨコ',
+ giftRatings: {
+ 'エイ~ブックレア': 'sa',
+ 'エーポッドプロ': 'na',
+ 'ゲームガールカラー復刻版': 'nb',
+ 'ぜんまい式オルゴール': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'kayoko_newyear',
+ name: 'カヨコ(正月)',
+ giftRatings: {
+ '音楽演奏会入場券': 'ss',
+ 'エイ~ブックレア': 'sa',
+ 'おしゃれなくし': 'sa',
+ 'エーポッドプロ': 'na',
+ '刺繍付きのハンカチ': 'na',
+ 'ゲームガールカラー復刻版': 'nb',
+ 'ぜんまい式オルゴール': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'kayoko_dress',
+ name: 'カヨコ(ドレス)',
+ giftRatings: {
+ 'エイ~ブックレア': 'ss',
+ 'エーポッドプロ': 'na',
+ 'ぜんまい式オルゴール': 'na',
+ 'ゲームガールカラー復刻版': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'karin',
+ name: 'カリン',
+ giftRatings: {
+ '裁縫キット': 'ss',
+ '頭脳開発キューブパズル': 'na',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'karin_schooluniform',
+ name: 'カリン(制服)',
+ giftRatings: {
+ '裁縫キット': 'ss',
+ '可愛い食器セット': 'na',
+ '禁断の愛~許されないからこそ美しく~': 'nb',
+ '刺繍付きのハンカチ': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'karin_bunnygirl',
+ name: 'カリン(バニーガール)',
+ giftRatings: {
+ '裁縫キット': 'ss',
+ 'リボンのついた熊のぬいぐるみ': 'na',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'kanna',
+ name: 'カンナ',
+ giftRatings: {
+ '栄養満載の総合ビタミンゼリー': 'ss',
+ 'ザ・サプリメント': 'na',
+ '百科事典': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'kanna_swimsuit',
+ name: 'カンナ(水着)',
+ giftRatings: {
+ 'ストリートオブヤンキー1巻': 'ss',
+ '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'na',
+ '夏模様の浮き輪': 'na',
+ '禁断の愛~許されないからこそ美しく~': 'nb',
+ 'ゲームマガジン「ヒットガールズ」': 'nb',
+ '『銃 可愛い 青春』': 'nb',
+ '古典の詩集': 'nb',
+ '百科事典': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'kikyou',
+ name: 'キキョウ',
+ giftRatings: {
+ 'ストリートオブヤンキー1巻': 'ss',
+ '『銃 可愛い 青春』': 'na',
+ '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'na',
+ '古典の詩集': 'na',
+ '百科事典': 'na',
+ '禁断の愛~許されないからこそ美しく~': 'nb',
+ 'ゲームマガジン「ヒットガールズ」': 'nb',
+ 'ザ・サプリメント': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'kikyou_swimsuit',
+ name: 'キキョウ(水着)',
+ giftRatings: {
+ 'ストリートオブヤンキー1巻': 'ss',
+ '『銃 可愛い 青春』': 'na',
+ '百科事典': 'na',
+ '夏模様の浮き輪': 'na',
+ '禁断の愛~許されないからこそ美しく~': 'nb',
+ 'ゲームマガジン「ヒットガールズ」': 'nb',
+ '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'nb',
+ '古典の詩集': 'nb',
+ 'ザ・サプリメント': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'kisaki',
+ name: 'キサキ',
+ giftRatings: {
+ '最高級の楓の盆栽': 'ss',
+ 'ストリートオブヤンキー1巻': 'sa',
+ '可愛い食器セット': 'na',
+ '禁断の愛~許されないからこそ美しく~': 'nb',
+ 'ゲームマガジン「ヒットガールズ」': 'nb',
+ '『銃 可愛い 青春』': 'nb',
+ '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'nb',
+ 'コスプレ用ぐるぐるメガネ': 'nb',
+ '古典の詩集': 'nb',
+ '食虫植物の植木鉢': 'nb',
+ '高級そうな欲望のつぼ': 'nb',
+ '百科事典': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'kirara',
+ name: 'キララ',
+ giftRatings: {
+ 'おしゃれなくし': 'ss',
+ 'チェリーローズカラーのグロス': 'na',
+ 'お肌を透明にするBBクリームス': 'na',
+ 'ミリタリー用カモフラージュクリーム3種セット': 'nb',
+ '刺繍付きのハンカチ': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'kirino',
+ name: 'キリノ',
+ giftRatings: {
+ 'ミルフィーユの正統派パフェ': 'ss',
+ '刺繍付きのハンカチ': 'na',
+ '高級なクッキーセット': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'kirino_swimsuit',
+ name: 'キリノ(水着)',
+ giftRatings: {
+ '1日3回ダンベルセット': 'ss',
+ 'O-フィット': 'na',
+ '夏模様の浮き輪': 'na',
+ 'ペロロの腹筋ローラー': 'nb',
+ '高級なクッキーセット': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'kurumi',
+ name: 'クルミ',
+ giftRatings: {
+ 'ボードゲーム「ザ・人生」': 'ss',
+ 'お肌を透明にするBBクリーム': 'na',
+ 'ミリタリー用カモフラージュクリーム3種セット': 'nb',
+ 'MX-レーションC型デザート風味': 'nb',
+ 'リボンのついた熊のぬいぐるみ': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'kei',
+ name: 'ケイ',
+ giftRatings: {
+ 'サミュエラ「ザ・ビヨンド」': 'ss',
+ 'チェリーローズカラーのグロス': 'na',
+ 'ゲームガールカラー復刻版': 'na',
+ 'ゲームマガジン「ヒットガールズ」': 'nb',
+ 'お肌を透明にするBBクリーム': 'nb',
+ 'ミリタリー用カモフラージュクリーム3種セット': 'nb',
+ 'O-フィット': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'kokona',
+ name: 'ココナ',
+ giftRatings: {
+ 'ミルフィーユの正統派パフェ': 'ss',
+ '高級なクッキーセット': 'na',
+ 'MX-レーションC型デザート風味': 'nb',
+ '抹茶味の瓶ラムネ': 'nb',
+ '大きなホールケーキ': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'kotama',
+ name: 'コタマ',
+ giftRatings: {
+ 'ボードゲーム「ザ・人生」': 'sa',
+ 'リボンのついた熊のぬいぐるみ': 'na',
+ '天体望遠鏡': 'nb',
+ 'コスプレ用ぐるぐるメガネ': 'nb',
+ '30色の絵の具セット': 'nb',
+ '頭脳開発キューブパズル': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'kotama_camping',
+ name: 'コタマ(キャンプ)',
+ giftRatings: {
+ 'エイ~ブックレア': 'ss',
+ 'ボードゲーム「ザ・人生」': 'sa',
+ 'リボンのついた熊のぬいぐるみ': 'na',
+ '天体望遠鏡': 'nb',
+ 'コスプレ用ぐるぐるメガネ': 'nb',
+ '30色の絵の具セット': 'nb',
+ '頭脳開発キューブパズル': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'kotori',
+ name: 'コトリ',
+ giftRatings: {
+ 'エイ~ブックレア': 'ss',
+ 'ストリートオブヤンキー1巻': 'sa',
+ '百科事典': 'na',
+ '禁断の愛~許されないからこそ美しく~': 'nb',
+ 'ゲームマガジン「ヒットガールズ」': 'nb',
+ '『銃 可愛い 青春』': 'nb',
+ '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'nb',
+ '古典の詩集': 'nb',
+ 'ザ・サプリメント': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'kotori_cheerleader',
+ name: 'コトリ(応援団)',
+ giftRatings: {
+ '1日3回ダンベルセット': 'ss',
+ '百科事典': 'na',
+ 'ペロロの腹筋ローラー': 'nb',
+ 'O-フィット': 'nb',
+ '頭脳開発キューブパズル': 'nb',
+ 'ザ・サプリメント': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'konoka',
+ name: 'コノカ',
+ giftRatings: {
+ '1日3回ダンベルセット': 'ss',
+ 'ストリートオブヤンキー1巻': 'sa',
+ 'レースの枕': 'sa',
+ 'ペロロの腹筋ローラー': 'na',
+ 'ウェーブキャットの枕': 'nb',
+ '禁断の愛~許されないからこそ美しく~': 'nb',
+ 'ゲームマガジン「ヒットガールズ」': 'nb',
+ '『銃 可愛い 青春』': 'nb',
+ 'ゼリーズの枕': 'nb',
+ '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'nb',
+ 'O-フィット': 'nb',
+ '古典の詩集': 'nb',
+ '百科事典': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'koharu',
+ name: 'コハル',
+ giftRatings: {
+ 'サミュエラ「ザ・ビヨンド」': 'sa',
+ '禁断の愛~許されないからこそ美しく~': 'na',
+ 'チェリーローズカラーのグロス': 'na',
+ 'お肌を透明にするBBクリーム': 'nb',
+ 'ミリタリー用カモフラージュクリーム3種セット': 'nb',
+ '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'koharu_swimsuit',
+ name: 'コハル(水着)',
+ giftRatings: {
+ 'サミュエラ「ザ・ビヨンド」': 'ss',
+ '禁断の愛~許されないからこそ美しく~': 'na',
+ '夏模様の浮き輪': 'na',
+ '『銃 可愛い 青春』': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'koyuki',
+ name: 'コユキ',
+ giftRatings: {
+ 'ボードゲーム「ザ・人生」': 'ss',
+ '世界で最も無駄な絡繰りボックス': 'na',
+ '頭脳開発キューブパズル': 'na',
+ '天体望遠鏡': 'nb',
+ 'コスプレ用ぐるぐるメガネ': 'nb',
+ 'リボンのついた熊のぬいぐるみ': 'nb',
+ '30色の絵の具セット': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'koyuki_pajama',
+ name: 'コユキ(パジャマ)',
+ giftRatings: {
+ 'ボードゲーム「ザ・人生」': 'ss',
+ 'レースの枕': 'sa',
+ 'ウェーブキャットの枕': 'na',
+ 'ゼリーズの枕': 'nb',
+ 'コスプレ用ぐるぐるメガネ': 'nb',
+ '食虫植物の植木鉢': 'nb',
+ '高級そうな欲望のつぼ': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'saori',
+ name: 'サオリ',
+ giftRatings: {
+ 'サミュエラ「ザ・ビヨンド」': 'sa',
+ 'チェリーローズカラーのグロス': 'na',
+ 'お肌を透明にするBBクリーム': 'na',
+ 'ミリタリー用カモフラージュクリーム3種セット': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'saori_swimsuit',
+ name: 'サオリ(水着)',
+ giftRatings: {
+ 'おしゃれなくし': 'ss',
+ 'サミュエラ「ザ・ビヨンド」': 'sa',
+ 'チェリーローズカラーのグロス': 'na',
+ 'お肌を透明にするBBクリーム': 'na',
+ 'ミリタリー用カモフラージュクリーム3種セット': 'na',
+ '夏模様の浮き輪': 'na',
+ 'MX-レーションC型デザート風味': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'saori_dress',
+ name: 'サオリ(ドレス)',
+ giftRatings: {
+ 'おしゃれなくし': 'ss',
+ 'サミュエラ「ザ・ビヨンド」': 'sa',
+ 'チェリーローズカラーのグロス': 'na',
+ 'お肌を透明にするBBクリーム': 'nb',
+ 'ミリタリー用カモフラージュクリーム3種セット': 'nb',
+ '刺繍付きのハンカチ': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'saki',
+ name: 'サキ',
+ giftRatings: {
+ 'レトロな卵の工芸品': 'sa',
+ 'ミリタリー用カモフラージュクリーム3種セット': 'na',
+ 'MX-レーションC型デザート風味': 'nb',
+ '埋蔵金の地図': 'nb',
+ '世界で最も無駄な絡繰りボックス': 'nb',
+ '高級そうな欲望のつぼ': 'nb',
+ 'ぜんまい式オルゴール': 'nb',
+ '百科事典': 'nb',
+ 'ザ・サプリメント': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'saki_swimsuit',
+ name: 'サキ(水着)',
+ giftRatings: {
+ '栄養満載の総合ビタミンゼリー': 'ss',
+ 'レトロな卵の工芸品': 'sa',
+ 'ミリタリー用カモフラージュクリーム3種セット': 'na',
+ '夏模様の浮き輪': 'na',
+ 'MX-レーションC型デザート風味': 'nb',
+ '埋蔵金の地図': 'nb',
+ '世界で最も無駄な絡繰りボックス': 'nb',
+ '高級そうな欲望のつぼ': 'nb',
+ 'ぜんまい式オルゴール': 'nb',
+ '百科事典': 'nb',
+ 'ザ・サプリメント': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'sakurako',
+ name: 'サクラコ',
+ giftRatings: {
+ 'ミルフィーユの正統派パフェ': 'ss',
+ '大きなホールケーキ': 'na',
+ '高級なクッキーセット': 'nb',
+ 'MX-レーションC型デザート風味': 'nb',
+ '抹茶味の瓶ラムネ': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'sakurako_idol',
+ name: 'サクラコ(アイドル)',
+ giftRatings: {
+ 'ミルフィーユの正統派パフェ': 'ss',
+ 'ストリートオブヤンキー1巻': 'sa',
+ 'ザ・サプリメント': 'na',
+ '禁断の愛~許されないからこそ美しく~': 'nb',
+ 'ゲームマガジン「ヒットガールズ」': 'nb',
+ '『銃 可愛い 青春』': 'nb',
+ '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'nb',
+ 'コスプレ用ぐるぐるメガネ': 'nb',
+ '世界で最も無駄な絡繰りボックス': 'nb',
+ '古典の詩集': 'nb',
+ '食虫植物の植木鉢': 'nb',
+ '高級そうな欲望のつぼ': 'nb',
+ '百科事典': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'satsuki',
+ name: 'サツキ',
+ giftRatings: {
+ 'レトロな卵の工芸品': 'ss',
+ 'ストリートオブヤンキー1巻': 'sa',
+ '百科事典': 'na',
+ '禁断の愛~許されないからこそ美しく~': 'nb',
+ 'ゲームマガジン「ヒットガールズ」': 'nb',
+ '『銃 可愛い 青春』': 'nb',
+ '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'nb',
+ '世界で最も無駄な絡繰りボックス': 'nb',
+ '古典の詩集': 'nb',
+ 'ザ・サプリメント': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'saya',
+ name: 'サヤ',
+ giftRatings: {
+ '栄養満載の総合ビタミンゼリー': 'sa',
+ 'ザ・サプリメント': 'na',
+ '百科事典': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'saya_casual',
+ name: 'サヤ(私服)',
+ giftRatings: {
+ '栄養満載の総合ビタミンゼリー': 'ss',
+ 'コスプレ用ぐるぐるメガネ': 'na',
+ '高級なクッキーセット': 'nb',
+ '食虫植物の植木鉢': 'nb',
+ '高級そうな欲望のつぼ': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'sigure',
+ name: 'シグレ',
+ giftRatings: {
+ '栄養満載の総合ビタミンゼリー': 'ss',
+ 'ザ・サプリメント': 'na',
+ '百科事典': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'sigure_hotspring',
+ name: 'シグレ(温泉)',
+ giftRatings: {
+ '栄養満載の総合ビタミンゼリー': 'ss',
+ 'ザ・サプリメント': 'na',
+ '百科事典': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'shizuko',
+ name: 'シズコ',
+ giftRatings: {
+ '栄養満載の総合ビタミンゼリー': 'sa',
+ 'ザ・サプリメント': 'na',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'shizuko_swimsuit',
+ name: 'シズコ(水着)',
+ giftRatings: {
+ '栄養満載の総合ビタミンゼリー': 'sa',
+ 'ザ・サプリメント': 'na',
+ '夏模様の浮き輪': 'na',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'shimiko',
+ name: 'シミコ',
+ giftRatings: {
+ 'ストリートオブヤンキー1巻': 'sa',
+ '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'na',
+ '禁断の愛~許されないからこそ美しく~': 'nb',
+ 'ゲームマガジン「ヒットガールズ」': 'nb',
+ '『銃 可愛い 青春』': 'nb',
+ '古典の詩集': 'nb',
+ '百科事典': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'juri',
+ name: 'ジュリ',
+ giftRatings: {
+ '栄養満載の総合ビタミンゼリー': 'ss',
+ '30色の絵の具セット': 'na',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'juri_parttimer',
+ name: 'ジュリ(アルバイト)',
+ giftRatings: {
+ 'ミルフィーユの正統派パフェ': 'ss',
+ '刺繍付きのハンカチ': 'na',
+ 'チェリーローズカラーのグロス': 'nb',
+ '可愛い食器セット': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'shun',
+ name: 'シュン',
+ giftRatings: {
+ 'サミュエラ「ザ・ビヨンド」': 'sa',
+ 'お肌を透明にするBBクリーム': 'na',
+ 'チェリーローズカラーのグロス': 'nb',
+ 'ミリタリー用カモフラージュクリーム3種セット': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'shun_kid',
+ name: 'シュン(幼女)',
+ giftRatings: {
+ 'ミルフィーユの正統派パフェ': 'sa',
+ '高級なクッキーセット': 'na',
+ 'MX-レーションC型デザート風味': 'nb',
+ '抹茶味の瓶ラムネ': 'nb',
+ 'リボンのついた熊のぬいぐるみ': 'nb',
+ '大きなホールケーキ': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'junko',
+ name: 'ジュンコ',
+ giftRatings: {
+ 'ミルフィーユの正統派パフェ': 'sa',
+ '高級なクッキーセット': 'na',
+ 'MX-レーションC型デザート風味': 'nb',
+ '抹茶味の瓶ラムネ': 'nb',
+ '大きなホールケーキ': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'junko_newyear',
+ name: 'ジュンコ(正月)',
+ giftRatings: {
+ 'ミルフィーユの正統派パフェ': 'ss',
+ '高級なクッキーセット': 'na',
+ 'MX-レーションC型デザート風味': 'nb',
+ '抹茶味の瓶ラムネ': 'nb',
+ '大きなホールケーキ': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'siroko',
+ name: 'シロコ',
+ giftRatings: {
+ '1日3回ダンベルセット': 'sa',
+ 'ペロロの腹筋ローラー': 'na',
+ 'O-フィット': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'siroko_riding',
+ name: 'シロコ(ライディング)',
+ giftRatings: {
+ '1日3回ダンベルセット': 'sa',
+ '埋蔵金の地図': 'na',
+ 'ペロロの腹筋ローラー': 'nb',
+ 'O-フィット': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'siroko_swimsuit',
+ name: 'シロコ(水着)',
+ giftRatings: {
+ '1日3回ダンベルセット': 'ss',
+ 'ペロロの腹筋ローラー': 'na',
+ '夏模様の浮き輪': 'na',
+ 'O-フィット': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'siroko_terror',
+ name: 'シロコ*テラー',
+ giftRatings: {
+ '1日3回ダンベルセット': 'sa',
+ 'ペロロの腹筋ローラー': 'na',
+ 'O-フィット': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'suzumi',
+ name: 'スズミ',
+ giftRatings: {
+ '音楽演奏会入場券': 'ss',
+ 'リボンのついた熊のぬいぐるみ': 'na',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'suzumi_magical',
+ name: 'スズミ(マジカル)',
+ giftRatings: {
+ 'おしゃれなくし': 'ss',
+ 'レースの枕': 'sa',
+ 'サミュエラ「ザ・ビヨンド」': 'sa',
+ 'エイ~ブックレア': 'sa',
+ 'エーポッドプロ': 'na',
+ 'お肌を透明にするBBクリーム': 'na',
+ 'ウェーブキャットの枕': 'nb',
+ 'チェリーローズカラーのグロス': 'nb',
+ 'ミリタリー用カモフラージュクリーム3種セット': 'nb',
+ 'ゲームガールカラー復刻版': 'nb',
+ 'ゼリーズの枕': 'nb',
+ 'ぜんまい式オルゴール': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'subaru',
+ name: 'スバル',
+ giftRatings: {
+ '音楽演奏会入場券': 'ss',
+ 'レトロな卵の工芸品': 'sa',
+ '埋蔵金の地図': 'na',
+ '大きなホールケーキ': 'na',
+ 'ぜんまい式オルゴール': 'na',
+ 'エーポッドプロ': 'nb',
+ 'チェリーローズカラーのグロス': 'nb',
+ 'ミリタリー用カモフラージュクリーム3種セット': 'nb',
+ 'MX-レーションC型デザート風味': 'nb',
+ '世界で最も無駄な絡繰りボックス': 'nb',
+ '高級そうな欲望のつぼ': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'sumire',
+ name: 'スミレ',
+ giftRatings: {
+ '1日3回ダンベルセット': 'sa',
+ 'O-フィット': 'na',
+ 'ペロロの腹筋ローラー': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'sumire_parttimer',
+ name: 'スミレ(アルバイト)',
+ giftRatings: {
+ '栄養満載の総合ビタミンゼリー': 'ss',
+ '1日3回ダンベルセット': 'sa',
+ 'O-フィット': 'na',
+ 'ペロロの腹筋ローラー': 'nb',
+ 'ザ・サプリメント': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'seia',
+ name: 'セイア',
+ giftRatings: {
+ 'ストリートオブヤンキー1巻': 'ss',
+ '古典の詩集': 'na',
+ '禁断の愛~許されないからこそ美しく~': 'nb',
+ 'ゲームマガジン「ヒットガールズ」': 'nb',
+ '『銃 可愛い 青春』': 'nb',
+ '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'nb',
+ 'コスプレ用ぐるぐるメガネ': 'nb',
+ '食虫植物の植木鉢': 'nb',
+ '高級そうな欲望のつぼ': 'nb',
+ '百科事典': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'seia_swimsuit',
+ name: 'セイア(水着)',
+ giftRatings: {
+ 'ボードゲーム「ザ・人生」': 'ss',
+ 'ストリートオブヤンキー1巻': 'sa',
+ 'ゲームガールカラー復刻版': 'na',
+ '夏模様の浮き輪': 'na',
+ '禁断の愛~許されないからこそ美しく~': 'nb',
+ 'ゲームマガジン「ヒットガールズ」': 'nb',
+ '『銃 可愛い 青春』': 'nb',
+ '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'nb',
+ 'コスプレ用ぐるぐるメガネ': 'nb',
+ '古典の詩集': 'nb',
+ '食虫植物の植木鉢': 'nb',
+ '高級そうな欲望のつぼ': 'nb',
+ '百科事典': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'sena',
+ name: 'セナ',
+ giftRatings: {
+ '栄養満載の総合ビタミンゼリー': 'ss',
+ 'レースの枕': 'sa',
+ 'ウェーブキャットの枕': 'na',
+ 'ゼリーズの枕': 'nb',
+ '百科事典': 'nb',
+ 'ザ・サプリメント': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'sena_casual',
+ name: 'セナ(私服)',
+ giftRatings: {
+ '栄養満載の総合ビタミンゼリー': 'ss',
+ 'ザ・サプリメント': 'na',
+ 'コスプレ用ぐるぐるメガネ': 'nb',
+ '食虫植物の植木鉢': 'nb',
+ '高級そうな欲望のつぼ': 'nb',
+ '百科事典': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'serika',
+ name: 'セリカ',
+ giftRatings: {
+ 'レトロな卵の工芸品': 'sa',
+ '高級そうな欲望のつぼ': 'na',
+ '埋蔵金の地図': 'nb',
+ '世界で最も無駄な絡繰りボックス': 'nb',
+ 'ぜんまい式オルゴール': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'serika_newyear',
+ name: 'セリカ(正月)',
+ giftRatings: {
+ 'レトロな卵の工芸品': 'sa',
+ '高級そうな欲望のつぼ': 'na',
+ '埋蔵金の地図': 'nb',
+ '世界で最も無駄な絡繰りボックス': 'nb',
+ 'ぜんまい式オルゴール': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'serika_swimsuit',
+ name: 'セリカ(水着)',
+ giftRatings: {
+ 'レトロな卵の工芸品': 'ss',
+ '高級そうな欲望のつぼ': 'na',
+ '夏模様の浮き輪': 'na',
+ '埋蔵金の地図': 'nb',
+ '世界で最も無駄な絡繰りボックス': 'nb',
+ 'ぜんまい式オルゴール': 'nb',
+ '刺繍付きのハンカチ': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'serina',
+ name: 'セリナ',
+ giftRatings: {
+ 'サミュエラ「ザ・ビヨンド」': 'ss',
+ '栄養満載の総合ビタミンゼリー': 'ss',
+ 'ゼリーズの枕': 'na',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'serina_christmas',
+ name: 'セリナ(クリスマス)',
+ giftRatings: {
+ 'レースの枕': 'ss',
+ '栄養満載の総合ビタミンゼリー': 'sa',
+ 'ゼリーズの枕': 'na',
+ 'ウェーブキャットの枕': 'nb',
+ 'ザ・サプリメント': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'takane',
+ name: 'タカネ',
+ giftRatings: {
+ '音楽演奏会入場券': 'ss',
+ 'ストリートオブヤンキー1巻': 'sa',
+ '『銃 可愛い 青春』': 'na',
+ '古典の詩集': 'na',
+ '刺繍付きのハンカチ': 'na',
+ '禁断の愛~許されないからこそ美しく~': 'nb',
+ 'ゲームマガジン「ヒットガールズ」': 'nb',
+ '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'nb',
+ '百科事典': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'chiaki',
+ name: 'チアキ',
+ giftRatings: {
+ 'エイ~ブックレア': 'ss',
+ 'ストリートオブヤンキー1巻': 'sa',
+ '古典の詩集': 'na',
+ '禁断の愛~許されないからこそ美しく~': 'nb',
+ 'ゲームマガジン「ヒットガールズ」': 'nb',
+ '『銃 可愛い 青春』': 'nb',
+ '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'nb',
+ '世界で最も無駄な絡繰りボックス': 'nb',
+ '百科事典': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'cherino',
+ name: 'チェリノ',
+ giftRatings: {
+ 'レトロな卵の工芸品': 'sa',
+ '高級そうな欲望のつぼ': 'na',
+ '埋蔵金の地図': 'nb',
+ 'コスプレ用ぐるぐるメガネ': 'nb',
+ '世界で最も無駄な絡繰りボックス': 'nb',
+ '大きなホールケーキ': 'nb',
+ '食虫植物の植木鉢': 'nb',
+ 'ぜんまい式オルゴール': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'cherino_hotspring',
+ name: 'チェリノ(温泉)',
+ giftRatings: {
+ 'レトロな卵の工芸品': 'ss',
+ '埋蔵金の地図': 'na',
+ 'コスプレ用ぐるぐるメガネ': 'nb',
+ '食虫植物の植木鉢': 'nb',
+ '高級そうな欲望のつぼ': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'chise',
+ name: 'チセ',
+ giftRatings: {
+ 'ストリートオブヤンキー1巻': 'sa',
+ '古典の詩集': 'na',
+ '禁断の愛~許されないからこそ美しく~': 'nb',
+ 'ゲームマガジン「ヒットガールズ」': 'nb',
+ '『銃 可愛い 青春』': 'nb',
+ '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'nb',
+ '百科事典': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'chise_swimsuit',
+ name: 'チセ(水着)',
+ giftRatings: {
+ 'ストリートオブヤンキー1巻': 'sa',
+ '古典の詩集': 'na',
+ '夏模様の浮き輪': 'na',
+ '禁断の愛~許されないからこそ美しく~': 'nb',
+ 'ゲームマガジン「ヒットガールズ」': 'nb',
+ '『銃 可愛い 青春』': 'nb',
+ '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'nb',
+ '百科事典': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'chinatsu',
+ name: 'チナツ',
+ giftRatings: {
+ 'レトロな卵の工芸品': 'ss',
+ 'ストリートオブヤンキー1巻': 'sa',
+ '『銃 可愛い 青春』': 'na',
+ '禁断の愛~許されないからこそ美しく~': 'nb',
+ 'ゲームマガジン「ヒットガールズ」': 'nb',
+ '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'nb',
+ '古典の詩集': 'nb',
+ '百科事典': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'chinatsu_hotspring',
+ name: 'チナツ(温泉)',
+ giftRatings: {
+ 'ストリートオブヤンキー1巻': 'sa',
+ 'レトロな卵の工芸品': 'sa',
+ '『銃 可愛い 青春』': 'na',
+ 'ぜんまい式オルゴール': 'na',
+ '禁断の愛~許されないからこそ美しく~': 'nb',
+ 'ゲームマガジン「ヒットガールズ」': 'nb',
+ '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'nb',
+ '埋蔵金の地図': 'nb',
+ '世界で最も無駄な絡繰りボックス': 'nb',
+ '古典の詩集': 'nb',
+ '高級そうな欲望のつぼ': 'nb',
+ '百科事典': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'chihiro',
+ name: 'チヒロ',
+ giftRatings: {
+ 'エイ~ブックレア': 'sa',
+ 'エーポッドプロ': 'na',
+ 'ゲームガールカラー復刻版': 'nb',
+ 'ぜんまい式オルゴール': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'tsukuyo',
+ name: 'ツクヨ',
+ giftRatings: {
+ '最高級の楓の盆栽': 'sa',
+ '食虫植物の植木鉢': 'na',
+ 'コスプレ用ぐるぐるメガネ': 'nb',
+ '高級そうな欲望のつぼ': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'tsukuyo_dress',
+ name: 'ツクヨ(ドレス)',
+ giftRatings: {
+ 'サミュエラ「ザ・ビヨンド」': 'ss',
+ 'おしゃれなくし': 'sa',
+ 'お肌を透明にするBBクリーム': 'na',
+ 'チェリーローズカラーのグロス': 'nb',
+ 'ミリタリー用カモフラージュクリーム3種セット': 'nb',
+ 'コスプレ用ぐるぐるメガネ': 'nb',
+ '食虫植物の植木鉢': 'nb',
+ '高級そうな欲望のつぼ': 'nb',
+ '刺繍付きのハンカチ': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'tsubaki',
+ name: 'ツバキ',
+ giftRatings: {
+ '最高級の楓の盆栽': 'ss',
+ 'レースの枕': 'sa',
+ 'ウェーブキャットの枕': 'na',
+ 'ゼリーズの枕': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'tsubaki_guide',
+ name: 'ツバキ(ガイド)',
+ giftRatings: {
+ 'レースの枕': 'ss',
+ 'ストリートオブヤンキー1巻': 'sa',
+ 'ウェーブキャットの枕': 'na',
+ '禁断の愛~許されないからこそ美しく~': 'nb',
+ 'ゲームマガジン「ヒットガールズ」': 'nb',
+ '『銃 可愛い 青春』': 'nb',
+ 'ゼリーズの枕': 'nb',
+ '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'nb',
+ '古典の詩集': 'nb',
+ '百科事典': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'tsurugi',
+ name: 'ツルギ',
+ giftRatings: {
+ 'おしゃれなくし': 'ss',
+ '禁断の愛~許されないからこそ美しく~': 'na',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'tsurugi_swimsuit',
+ name: 'ツルギ(水着)',
+ giftRatings: {
+ 'おしゃれなくし': 'ss',
+ '禁断の愛~許されないからこそ美しく~': 'na',
+ '夏模様の浮き輪': 'na',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'toki',
+ name: 'トキ',
+ giftRatings: {
+ '1日3回ダンベルセット': 'ss',
+ 'ペロロの腹筋ローラー': 'na',
+ 'O-フィット': 'nb',
+ '百科事典': 'nb',
+ 'ザ・サプリメント': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'toki_bunnygirl',
+ name: 'トキ(バニーガール)',
+ giftRatings: {
+ '裁縫キット': 'ss',
+ '1日3回ダンベルセット': 'ss',
+ 'ペロロの腹筋ローラー': 'na',
+ 'O-フィット': 'nb',
+ '百科事典': 'nb',
+ 'ザ・サプリメント': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'toki_battle',
+ name: 'トキ(臨戦)',
+ giftRatings: {
+ 'ボードゲーム「ザ・人生」': 'ss',
+ 'エイ~ブックレア': 'sa',
+ 'ゲームガールカラー復刻版': 'na',
+ '映画の前売りペアチケット': 'na',
+ 'エーポッドプロ': 'nb',
+ 'ゲームマガジン「ヒットガールズ」': 'nb',
+ 'O-フィット': 'nb',
+ 'コスプレ用ぐるぐるメガネ': 'nb',
+ '食虫植物の植木鉢': 'nb',
+ '高級そうな欲望のつぼ': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'tomoe',
+ name: 'トモエ',
+ giftRatings: {
+ 'レトロな卵の工芸品': 'ss',
+ '古典の詩集': 'na',
+ '『銃 可愛い 青春』': 'nb',
+ '刺繍付きのハンカチ': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'tomoe_qipao',
+ name: 'トモエ(チーパオ)',
+ giftRatings: {
+ 'サミュエラ「ザ・ビヨンド」': 'ss',
+ 'ストリートオブヤンキー1巻': 'sa',
+ 'おしゃれなくし': 'sa',
+ 'チェリーローズカラーのグロス': 'na',
+ '禁断の愛~許されないからこそ美しく~': 'nb',
+ 'ゲームマガジン「ヒットガールズ」': 'nb',
+ '『銃 可愛い 青春』': 'nb',
+ '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'nb',
+ '古典の詩集': 'nb',
+ '刺繍付きのハンカチ': 'nb',
+ '百科事典': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'nagisa',
+ name: 'ナギサ',
+ giftRatings: {
+ 'レトロな卵の工芸品': 'ss',
+ 'ぜんまい式オルゴール': 'na',
+ '高級なクッキーセット': 'nb',
+ '埋蔵金の地図': 'nb',
+ '世界で最も無駄な絡繰りボックス': 'nb',
+ '高級そうな欲望のつぼ': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'nagisa_swimsuit',
+ name: 'ナギサ(水着)',
+ giftRatings: {
+ 'レースの枕': 'ss',
+ 'レトロな卵の工芸品': 'sa',
+ '高級なクッキーセット': 'na',
+ '夏模様の浮き輪': 'na',
+ '埋蔵金の地図': 'nb',
+ '世界で最も無駄な絡繰りボックス': 'nb',
+ '高級そうな欲望のつぼ': 'nb',
+ 'ぜんまい式オルゴール': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'nagusa',
+ name: 'ナグサ',
+ giftRatings: {
+ 'おしゃれなくし': 'ss',
+ 'ミルフィーユの正統派パフェ': 'sa',
+ '古典の詩集': 'na',
+ 'お肌を透明にするBBクリーム': 'nb',
+ '高級なクッキーセット': 'nb',
+ 'MX-レーションC型デザート風味': 'nb',
+ '抹茶味の瓶ラムネ': 'nb',
+ '大きなホールケーキ': 'nb',
+ '刺繍付きのハンカチ': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'natsu',
+ name: 'ナツ',
+ giftRatings: {
+ 'ミルフィーユの正統派パフェ': 'sa',
+ '埋蔵金の地図': 'na',
+ '高級なクッキーセット': 'nb',
+ 'MX-レーションC型デザート風味': 'nb',
+ '抹茶味の瓶ラムネ': 'nb',
+ '大きなホールケーキ': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'natsu_band',
+ name: 'ナツ(バンド)',
+ giftRatings: {
+ 'ミルフィーユの正統派パフェ': 'ss',
+ '高級なクッキーセット': 'na',
+ '大きなホールケーキ': 'na',
+ 'MX-レーションC型デザート風味': 'nb',
+ '抹茶味の瓶ラムネ': 'nb',
+ '埋蔵金の地図': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'niko',
+ name: 'ニコ',
+ giftRatings: {
+ '裁縫キット': 'ss',
+ 'ミルフィーユの正統派パフェ': 'sa',
+ 'MX-レーションC型デザート風味': 'na',
+ 'ミリタリー用カモフラージュクリーム3種セット': 'nb',
+ '高級なクッキーセット': 'nb',
+ '抹茶味の瓶ラムネ': 'nb',
+ '可愛い食器セット': 'nb',
+ '大きなホールケーキ': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'niya',
+ name: 'ニヤ',
+ giftRatings: {
+ '最高級の楓の盆栽': 'ss',
+ 'レトロな卵の工芸品': 'sa',
+ 'ボードゲーム「ザ・人生」': 'sa',
+ '禁断の愛~許されないからこそ美しく~': 'na',
+ 'コスプレ用ぐるぐるメガネ': 'na',
+ '高級そうな欲望のつぼ': 'na',
+ '天体望遠鏡': 'nb',
+ '埋蔵金の地図': 'nb',
+ '世界で最も無駄な絡繰りボックス': 'nb',
+ 'リボンのついた熊のぬいぐるみ': 'nb',
+ '30色の絵の具セット': 'nb',
+ '頭脳開発キューブパズル': 'nb',
+ '食虫植物の植木鉢': 'nb',
+ 'ぜんまい式オルゴール': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'neru',
+ name: 'ネル',
+ giftRatings: {
+ 'ボードゲーム「ザ・人生」': 'sa',
+ 'コスプレ用ぐるぐるメガネ': 'na',
+ '天体望遠鏡': 'nb',
+ 'リボンのついた熊のぬいぐるみ': 'nb',
+ '30色の絵の具セット': 'nb',
+ '頭脳開発キューブパズル': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'neru_schooluniform',
+ name: 'ネル(制服)',
+ giftRatings: {
+ 'ボードゲーム「ザ・人生」': 'ss',
+ 'コスプレ用ぐるぐるメガネ': 'na',
+ 'ゲームマガジン「ヒットガールズ」': 'nb',
+ '天体望遠鏡': 'nb',
+ 'ゲームガールカラー復刻版': 'nb',
+ 'O-フィット': 'nb',
+ 'リボンのついた熊のぬいぐるみ': 'nb',
+ '30色の絵の具セット': 'nb',
+ '頭脳開発キューブパズル': 'nb',
+ '食虫植物の植木鉢': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'neru_bunnygirl',
+ name: 'ネル(バニーガール)',
+ giftRatings: {
+ 'ボードゲーム「ザ・人生」': 'ss',
+ 'ゲームガールカラー復刻版': 'na',
+ 'ゲームマガジン「ヒットガールズ」': 'nb',
+ 'O-フィット': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'noa',
+ name: 'ノア',
+ giftRatings: {
+ 'ストリートオブヤンキー1巻': 'sa',
+ '古典の詩集': 'na',
+ '禁断の愛~許されないからこそ美しく~': 'nb',
+ 'ゲームマガジン「ヒットガールズ」': 'nb',
+ '『銃 可愛い 青春』': 'nb',
+ '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'nb',
+ '百科事典': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'noa_pajama',
+ name: 'ノア(パジャマ)',
+ giftRatings: {
+ 'レースの枕': 'ss',
+ 'ストリートオブヤンキー1巻': 'sa',
+ 'サミュエラ「ザ・ビヨンド」': 'sa',
+ '古典の詩集': 'na',
+ 'ウェーブキャットの枕': 'nb',
+ '禁断の愛~許されないからこそ美しく~': 'nb',
+ 'ゲームマガジン「ヒットガールズ」': 'nb',
+ 'チェリーローズカラーのグロス': 'nb',
+ 'お肌を透明にするBBクリーム': 'nb',
+ 'ミリタリー用カモフラージュクリーム3種セット': 'nb',
+ '『銃 可愛い 青春』': 'nb',
+ 'ゼリーズの枕': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'nozomi',
+ name: 'ノゾミ',
+ giftRatings: {
+ 'ボードゲーム「ザ・人生」': 'ss',
+ 'レトロな卵の工芸品': 'sa',
+ 'コスプレ用ぐるぐるメガネ': 'na',
+ '天体望遠鏡': 'nb',
+ '埋蔵金の地図': 'nb',
+ '世界で最も無駄な絡繰りボックス': 'nb',
+ 'リボンのついた熊のぬいぐるみ': 'nb',
+ '30色の絵の具セット': 'nb',
+ '頭脳開発キューブパズル': 'nb',
+ '高級そうな欲望のつぼ': 'nb',
+ 'ぜんまい式オルゴール': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'nodoka',
+ name: 'ノドカ',
+ giftRatings: {
+ 'ミルフィーユの正統派パフェ': 'sa',
+ '天体望遠鏡': 'na',
+ '高級なクッキーセット': 'nb',
+ 'MX-レーションC型デザート風味': 'nb',
+ '抹茶味の瓶ラムネ': 'nb',
+ '大きなホールケーキ': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'nodoka_hotspring',
+ name: 'ノドカ(温泉)',
+ giftRatings: {
+ 'ミルフィーユの正統派パフェ': 'sa',
+ '音楽演奏会入場券': 'sa',
+ 'ハイクラスビュッフェ招待券': 'na',
+ '高級なクッキーセット': 'nb',
+ '天体望遠鏡': 'nb',
+ 'MX-レーションC型デザート風味': 'nb',
+ '抹茶味の瓶ラムネ': 'nb',
+ '映画の前売りペアチケット': 'nb',
+ '大きなホールケーキ': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'nonomi',
+ name: 'ノノミ',
+ giftRatings: {
+ 'サミュエラ「ザ・ビヨンド」': 'sa',
+ 'チェリーローズカラーのグロス': 'na',
+ 'お肌を透明にするBBクリーム': 'nb',
+ 'ミリタリー用カモフラージュクリーム3種セット': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'nonomi_swimsuit',
+ name: 'ノノミ(水着)',
+ giftRatings: {
+ 'サミュエラ「ザ・ビヨンド」': 'sa',
+ 'チェリーローズカラーのグロス': 'na',
+ '夏模様の浮き輪': 'na',
+ 'お肌を透明にするBBクリーム': 'nb',
+ 'ミリタリー用カモフラージュクリーム3種セット': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'hasumi',
+ name: 'ハスミ',
+ giftRatings: {
+ 'ミルフィーユの正統派パフェ': 'sa',
+ '大きなホールケーキ': 'na',
+ '高級なクッキーセット': 'nb',
+ 'MX-レーションC型デザート風味': 'nb',
+ '抹茶味の瓶ラムネ': 'nb',
+ 'ハイクラスビュッフェ招待券': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'hasumi_sportswear',
+ name: 'ハスミ(体操服)',
+ giftRatings: {
+ 'ミルフィーユの正統派パフェ': 'ss',
+ '大きなホールケーキ': 'na',
+ '高級なクッキーセット': 'nb',
+ 'MX-レーションC型デザート風味': 'nb',
+ '抹茶味の瓶ラムネ': 'nb',
+ 'ハイクラスビュッフェ招待券': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'hasumi_swimsuit',
+ name: 'ハスミ(水着)',
+ giftRatings: {
+ 'ミルフィーユの正統派パフェ': 'ss',
+ '大きなホールケーキ': 'na',
+ '夏模様の浮き輪': 'na',
+ '高級なクッキーセット': 'nb',
+ 'MX-レーションC型デザート風味': 'nb',
+ '抹茶味の瓶ラムネ': 'nb',
+ 'ハイクラスビュッフェ招待券': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'hanae',
+ name: 'ハナエ',
+ giftRatings: {
+ 'サミュエラ「ザ・ビヨンド」': 'sa',
+ 'お肌を透明にするBBクリーム': 'na',
+ 'チェリーローズカラーのグロス': 'nb',
+ 'ミリタリー用カモフラージュクリーム3種セット': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'hanae_christmas',
+ name: 'ハナエ(クリスマス)',
+ giftRatings: {
+ '1日3回ダンベルセット': 'ss',
+ '栄養満載の総合ビタミンゼリー': 'sa',
+ 'O-フィット': 'na',
+ 'ペロロの腹筋ローラー': 'nb',
+ 'ザ・サプリメント': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'hanako',
+ name: 'ハナコ',
+ giftRatings: {
+ 'ストリートオブヤンキー1巻': 'sa',
+ '禁断の愛~許されないからこそ美しく~': 'na',
+ '世界で最も無駄な絡繰りボックス': 'na',
+ 'ゲームマガジン「ヒットガールズ」': 'nb',
+ '『銃 可愛い 青春』': 'nb',
+ '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'nb',
+ '古典の詩集': 'nb',
+ '百科事典': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'hanako_swimsuit',
+ name: 'ハナコ(水着)',
+ giftRatings: {
+ 'おしゃれなくし': 'ss',
+ 'サミュエラ「ザ・ビヨンド」': 'sa',
+ 'お肌を透明にするBBクリーム': 'ns',
+ '夏模様の浮き輪': 'na',
+ 'チェリーローズカラーのグロス': 'nb',
+ 'ミリタリー用カモフラージュクリーム3種セット': 'nb',
+ '刺繍付きのハンカチ': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'haruka',
+ name: 'ハルカ',
+ giftRatings: {
+ '最高級の楓の盆栽': 'sa',
+ '食虫植物の植木鉢': 'na',
+ 'コスプレ用ぐるぐるメガネ': 'nb',
+ '高級そうな欲望のつぼ': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'haruka_newyear',
+ name: 'ハルカ(正月)',
+ giftRatings: {
+ '最高級の楓の盆栽': 'ss',
+ '食虫植物の植木鉢': 'na',
+ 'コスプレ用ぐるぐるメガネ': 'nb',
+ '高級そうな欲望のつぼ': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'ハルカ_ドレス',
+ name: 'ハルカ(ドレス)',
+ giftRatings: {
+ '最高級の楓の盆栽': 'ss',
+ 'レトロな卵の工芸品': 'sa',
+ 'ボードゲーム「ザ・人生」': 'sa',
+ '食虫植物の植木鉢': 'na',
+ '天体望遠鏡': 'nb',
+ '埋蔵金の地図': 'nb',
+ 'コスプレ用ぐるぐるメガネ': 'nb',
+ '世界で最も無駄な絡繰りボックス': 'nb',
+ 'リボンのついた熊のぬいぐるみ': 'nb',
+ '30色の絵の具セット': 'nb',
+ '頭脳開発キューブパズル': 'nb',
+ '高級そうな欲望のつぼ': 'nb',
+ 'ぜんまい式オルゴール': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'haruna',
+ name: 'ハルナ',
+ giftRatings: {
+ '音楽演奏会入場券': 'ss',
+ '刺繍付きのハンカチ': 'na',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'haruna_newyear',
+ name: 'ハルナ(正月)',
+ giftRatings: {
+ '音楽演奏会入場券': 'ss',
+ '高級なクッキーセット': 'na',
+ '刺繍付きのハンカチ': 'na',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'haruna_sportswear',
+ name: 'ハルナ(体操服)',
+ giftRatings: {
+ 'ミルフィーユの正統派パフェ': 'ss',
+ '1日3回ダンベルセット': 'sa',
+ '高級なクッキーセット': 'na',
+ 'ペロロの腹筋ローラー': 'nb',
+ 'MX-レーションC型デザート風味': 'nb',
+ '抹茶味の瓶ラムネ': 'nb',
+ 'O-フィット': 'nb',
+ '大きなホールケーキ': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'hare',
+ name: 'ハレ',
+ giftRatings: {
+ 'エイ~ブックレア': 'ss',
+ 'ゲームマガジン「ヒットガールズ」': 'na',
+ 'ゲームガールカラー復刻版': 'nb',
+ 'O-フィット': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'hare_camping',
+ name: 'ハレ(キャンプ)',
+ giftRatings: {
+ 'エイ~ブックレア': 'ss',
+ 'ゲームマガジン「ヒットガールズ」': 'na',
+ 'ゲームガールカラー復刻版': 'nb',
+ 'O-フィット': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'hikari',
+ name: 'ヒカリ',
+ giftRatings: {
+ 'ミルフィーユの正統派パフェ': 'ss',
+ '高級なクッキーセット': 'na',
+ 'ゲームマガジン「ヒットガールズ」': 'nb',
+ 'ゲームガールカラー復刻版': 'nb',
+ 'MX-レーションC型デザート風味': 'nb',
+ '抹茶味の瓶ラムネ': 'nb',
+ 'O-フィット': 'nb',
+ '大きなホールケーキ': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'hina',
+ name: 'ヒナ',
+ giftRatings: {
+ 'サミュエラ「ザ・ビヨンド」': 'ss',
+ '世界で最も無駄な絡繰りボックス': 'na',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'hina_swimsuit',
+ name: 'ヒナ(水着)',
+ giftRatings: {
+ 'サミュエラ「ザ・ビヨンド」': 'ss',
+ '世界で最も無駄な絡繰りボックス': 'na',
+ '夏模様の浮き輪': 'na',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'hina_dress',
+ name: 'ヒナ(ドレス)',
+ giftRatings: {
+ '音楽演奏会入場券': 'ss',
+ '世界で最も無駄な絡繰りボックス': 'na',
+ '映画の前売りペアチケット': 'na',
+ 'ハイクラスビュッフェ招待券': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'hinata',
+ name: 'ヒナタ',
+ giftRatings: {
+ '裁縫キット': 'ss',
+ '最高級の楓の盆栽': 'sa',
+ '食虫植物の植木鉢': 'na',
+ '刺繍付きのハンカチ': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'hinata_swimsuit',
+ name: 'ヒナタ(水着)',
+ giftRatings: {
+ '裁縫キット': 'ss',
+ '可愛い食器セット': 'na',
+ '夏模様の浮き輪': 'na',
+ '刺繍付きのハンカチ': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'hibiki',
+ name: 'ヒビキ',
+ giftRatings: {
+ 'エイ~ブックレア': 'sa',
+ 'エーポッドプロ': 'na',
+ 'ゲームガールカラー復刻版': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'hibiki_cheerleader',
+ name: 'ヒビキ(応援団)',
+ giftRatings: {
+ '裁縫キット': 'ss',
+ 'エイ~ブックレア': 'sa',
+ 'エーポッドプロ': 'na',
+ 'ゲームガールカラー復刻版': 'nb',
+ '可愛い食器セット': 'nb',
+ 'ぜんまい式オルゴール': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'hifumi',
+ name: 'ヒフミ',
+ giftRatings: {
+ 'サミュエラ「ザ・ビヨンド」': 'ss',
+ 'ペロロの腹筋ローラー': 'na',
+ 'ウェーブキャットの枕': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'hifumi_swimsuit',
+ name: 'ヒフミ(水着)',
+ giftRatings: {
+ 'レトロな卵の工芸品': 'ss',
+ 'ペロロの腹筋ローラー': 'na',
+ '夏模様の浮き輪': 'na',
+ 'ウェーブキャットの枕': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'himari',
+ name: 'ヒマリ',
+ giftRatings: {
+ 'レトロな卵の工芸品': 'ss',
+ '世界で最も無駄な絡繰りボックス': 'na',
+ '埋蔵金の地図': 'nb',
+ '高級そうな欲望のつぼ': 'nb',
+ 'ぜんまい式オルゴール': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'himari_battle',
+ name: 'ヒマリ(臨戦)',
+ giftRatings: {
+ 'レトロな卵の工芸品': 'ss',
+ '世界で最も無駄な絡繰りボックス': 'na',
+ '埋蔵金の地図': 'nb',
+ '高級そうな欲望のつぼ': 'nb',
+ 'ぜんまい式オルゴール': 'nb',
+ '百科事典': 'nb',
+ 'ザ・サプリメント': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'hiyori',
+ name: 'ヒヨリ',
+ giftRatings: {
+ '裁縫キット': 'sa',
+ '可愛い食器セット': 'na',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'hiyori_swimsuit',
+ name: 'ヒヨリ(水着)',
+ giftRatings: {
+ 'ミルフィーユの正統派パフェ': 'ss',
+ '禁断の愛~許されないからこそ美しく~': 'na',
+ '大きなホールケーキ': 'na',
+ '夏模様の浮き輪': 'na',
+ '高級なクッキーセット': 'nb',
+ 'MX-レーションC型デザート風味': 'nb',
+ '抹茶味の瓶ラムネ': 'nb',
+ 'ハイクラスビュッフェ招待券': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'pina',
+ name: 'フィーナ',
+ giftRatings: {
+ '裁縫キット': 'ss',
+ '映画の前売りペアチケット': 'na',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'pina_guide',
+ name: 'フィーナ(ガイド)',
+ giftRatings: {
+ 'ストリートオブヤンキー1巻': 'ss',
+ '音楽演奏会入場券': 'sa',
+ '映画の前売りペアチケット': 'na',
+ '禁断の愛~許されないからこそ美しく~': 'nb',
+ 'ゲームマガジン「ヒットガールズ」': 'nb',
+ '『銃 可愛い 青春』': 'nb',
+ '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'nb',
+ '古典の詩集': 'nb',
+ 'ハイクラスビュッフェ招待券': 'nb',
+ '百科事典': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'fuuka',
+ name: 'フウカ',
+ giftRatings: {
+ '裁縫キット': 'sa',
+ '可愛い食器セット': 'na',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'fuuka_newyear',
+ name: 'フウカ(正月)',
+ giftRatings: {
+ '栄養満載の総合ビタミンゼリー': 'ss',
+ '裁縫キット': 'sa',
+ '可愛い食器セット': 'na',
+ 'ザ・サプリメント': 'na',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'fubuki',
+ name: 'フブキ',
+ giftRatings: {
+ 'ミルフィーユの正統派パフェ': 'sa',
+ '高級なクッキーセット': 'na',
+ 'MX-レーションC型デザート風味': 'nb',
+ '抹茶味の瓶ラムネ': 'nb',
+ '大きなホールケーキ': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'fubuki_swimsuit',
+ name: 'フブキ(水着)',
+ giftRatings: {
+ 'ミルフィーユの正統派パフェ': 'ss',
+ '高級なクッキーセット': 'na',
+ '抹茶味の瓶ラムネ': 'na',
+ '夏模様の浮き輪': 'na',
+ 'ゲームマガジン「ヒットガールズ」': 'nb',
+ 'ゲームガールカラー復刻版': 'nb',
+ 'MX-レーションC型デザート風味': 'nb',
+ 'O-フィット': 'nb',
+ '大きなホールケーキ': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'fuyu',
+ name: 'フユ',
+ giftRatings: {
+ 'サミュエラ「ザ・ビヨンド」': 'ss',
+ '映画の前売りペアチケット': 'na',
+ 'チェリーローズカラーのグロス': 'nb',
+ 'お肌を透明にするBBクリーム': 'nb',
+ 'ミリタリー用カモフラージュクリーム3種セット': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'hoshino',
+ name: 'ホシノ',
+ giftRatings: {
+ 'レースの枕': 'ss',
+ 'レトロな卵の工芸品': 'ss',
+ '埋蔵金の地図': 'na',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'hoshino_swimsuit',
+ name: 'ホシノ(水着)',
+ giftRatings: {
+ 'レースの枕': 'ss',
+ 'レトロな卵の工芸品': 'ss',
+ '埋蔵金の地図': 'na',
+ '夏模様の浮き輪': 'na',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'hoshino_battle',
+ name: 'ホシノ(臨戦)',
+ giftRatings: {
+ 'レースの枕': 'ss',
+ 'レトロな卵の工芸品': 'sa',
+ 'ミリタリー用カモフラージュクリーム3種セット': 'na',
+ 'MX-レーションC型デザート風味': 'na',
+ '埋蔵金の地図': 'na',
+ '世界で最も無駄な絡繰りボックス': 'nb',
+ '高級そうな欲望のつぼ': 'nb',
+ 'ぜんまい式オルゴール': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'maki',
+ name: 'マキ',
+ giftRatings: {
+ 'ボードゲーム「ザ・人生」': 'sa',
+ '30色の絵の具セット': 'na',
+ '天体望遠鏡': 'nb',
+ 'コスプレ用ぐるぐるメガネ': 'nb',
+ 'リボンのついた熊のぬいぐるみ': 'nb',
+ '頭脳開発キューブパズル': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'maki_camping',
+ name: 'マキ(キャンプ)',
+ giftRatings: {
+ 'おしゃれなくし': 'ss',
+ 'エイ~ブックレア': 'sa',
+ 'ゲームガールカラー復刻版 ': 'na',
+ '30色の絵の具セット': 'na',
+ 'エーポッドプロ': 'nb',
+ 'ゲームマガジン「ヒットガールズ」': 'nb',
+ 'O-フィット': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'makoto',
+ name: 'マコト',
+ giftRatings: {
+ 'レトロな卵の工芸品': 'ss',
+ '埋蔵金の地図': 'na',
+ '世界で最も無駄な絡繰りボックス': 'na',
+ '高級そうな欲望のつぼ': 'na',
+ 'コスプレ用ぐるぐるメガネ': 'nb',
+ '食虫植物の植木鉢': 'nb',
+ 'ぜんまい式オルゴール': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'mashiro',
+ name: 'マシロ',
+ giftRatings: {
+ '栄養満載の総合ビタミンゼリー': 'ss',
+ 'ミリタリー用カモフラージュクリーム3種セット': 'na',
+ 'MX-レーションC型デザート風味': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'mashiro_swimsuit',
+ name: 'マシロ(水着)',
+ giftRatings: {
+ '栄養満載の総合ビタミンゼリー': 'ss',
+ 'MX-レーションC型デザート風味': 'na',
+ '夏模様の浮き輪': 'na',
+ 'ミリタリー用カモフラージュクリーム3種セット': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'mari',
+ name: 'マリー',
+ giftRatings: {
+ '最高級の楓の盆栽': 'sa',
+ '刺繍付きのハンカチ': 'na',
+ '食虫植物の植木鉢': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'mari_sportswear',
+ name: 'マリー(体操服)',
+ giftRatings: {
+ 'おしゃれなくし': 'ss',
+ '最高級の楓の盆栽': 'sa',
+ '刺繍付きのハンカチ': 'na',
+ '食虫植物の植木鉢': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'mari_idol',
+ name: 'マリー(アイドル)',
+ giftRatings: {
+ '裁縫キット': 'ss',
+ '可愛い食器セット': 'na',
+ '刺繍付きのハンカチ': 'na',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'marina',
+ name: 'マリナ',
+ giftRatings: {
+ 'レトロな卵の工芸品': 'sa',
+ 'リボンのついた熊のぬいぐるみ': 'na',
+ '埋蔵金の地図': 'nb',
+ '世界で最も無駄な絡繰りボックス': 'nb',
+ '高級そうな欲望のつぼ': 'nb',
+ 'ぜんまい式オルゴール': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'marina_qipao',
+ name: 'マリナ(チーパオ)',
+ giftRatings: {
+ 'おしゃれなくし': 'ss',
+ 'レトロな卵の工芸品': 'sa',
+ 'ボードゲーム「ザ・人生」': 'sa',
+ 'リボンのついた熊のぬいぐるみ': 'na',
+ '天体望遠鏡': 'nb',
+ '埋蔵金の地図': 'nb',
+ 'コスプレ用ぐるぐるメガネ': 'nb',
+ '世界で最も無駄な絡繰りボックス': 'nb',
+ '30色の絵の具セット': 'nb',
+ '頭脳開発キューブパズル': 'nb',
+ '高級そうな欲望のつぼ': 'nb',
+ 'ぜんまい式オルゴール': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'mika',
+ name: 'ミカ',
+ giftRatings: {
+ 'ミルフィーユの正統派パフェ': 'ss',
+ 'サミュエラ「ザ・ビヨンド」': 'sa',
+ 'お肌を透明にするBBクリーム': 'na',
+ 'チェリーローズカラーのグロス': 'nb',
+ 'ミリタリー用カモフラージュクリーム3種セット': 'nb',
+ '高級なクッキーセット': 'nb',
+ 'MX-レーションC型デザート風味': 'nb',
+ '抹茶味の瓶ラムネ': 'nb',
+ '大きなホールケーキ': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'mika_swimsuit',
+ name: 'ミカ(水着)',
+ giftRatings: {
+ 'おしゃれなくし': 'ss',
+ 'サミュエラ「ザ・ビヨンド」': 'sa',
+ 'チェリーローズカラーのグロス': 'na',
+ '夏模様の浮き輪': 'na',
+ 'お肌を透明にするBBクリーム': 'nb',
+ 'ミリタリー用カモフラージュクリーム3種セット': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'misaki',
+ name: 'ミサキ',
+ giftRatings: {
+ 'レースの枕': 'ss',
+ 'リボンのついた熊のぬいぐるみ': 'na',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'misaki_swimsuit',
+ name: 'ミサキ(水着)',
+ giftRatings: {
+ 'おしゃれなくし': 'ss',
+ 'リボンのついた熊のぬいぐるみ': 'na',
+ '夏模様の浮き輪': 'na',
+ '刺繍付きのハンカチ': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'michiru',
+ name: 'ミチル',
+ giftRatings: {
+ 'レトロな卵の工芸品': 'sa',
+ '埋蔵金の地図': 'na',
+ '世界で最も無駄な絡繰りボックス': 'nb',
+ '高級そうな欲望のつぼ': 'nb',
+ 'ぜんまい式オルゴール': 'nb',
+ '刺繍付きのハンカチ': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'michiru_dress',
+ name: 'ミチル(ドレス)',
+ giftRatings: {
+ 'おしゃれなくし': 'ss',
+ '埋蔵金の地図': 'na',
+ '高級そうな欲望のつぼ': 'nb',
+ 'コスプレ用ぐるぐるメガネ': 'nb',
+ '食虫植物の植木鉢': 'nb',
+ '刺繍付きのハンカチ': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'midori',
+ name: 'ミドリ',
+ giftRatings: {
+ 'ボードゲーム「ザ・人生」': 'ss',
+ 'ゼリーズの枕': 'na',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'midori_maid',
+ name: 'ミドリ(メイド)',
+ giftRatings: {
+ '裁縫キット': 'ss',
+ '可愛い食器セット': 'na',
+ 'ゲームマガジン「ヒットガールズ」': 'nb',
+ 'ゲームガールカラー復刻版': 'nb',
+ 'O-フィット': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'mina',
+ name: 'ミナ',
+ giftRatings: {
+ 'レトロな卵の工芸品': 'ss',
+ '高級そうな欲望のつぼ': 'na',
+ 'ぜんまい式オルゴール': 'na',
+ '埋蔵金の地図': 'nb',
+ 'コスプレ用ぐるぐるメガネ': 'nb',
+ '世界で最も無駄な絡繰りボックス': 'nb',
+ '食虫植物の植木鉢': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'mine',
+ name: 'ミネ',
+ giftRatings: {
+ 'サミュエラ「ザ・ビヨンド」': 'ss',
+ 'チェリーローズカラーのグロス': 'na',
+ 'お肌を透明にするBBクリーム': 'nb',
+ 'ミリタリー用カモフラージュクリーム3種セット': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'mine_idol',
+ name: 'ミネ(アイドル)',
+ giftRatings: {
+ 'おしゃれなくし': 'ss',
+ 'サミュエラ「ザ・ビヨンド」': 'sa',
+ 'ウェーブキャットの枕': 'na',
+ 'チェリーローズカラーのグロス': 'na',
+ 'リボンのついた熊のぬいぐるみ': 'na',
+ 'ペロロの腹筋ローラー': 'nb',
+ 'お肌を透明にするBBクリーム': 'nb',
+ 'ミリタリー用カモフラージュクリーム3種セット': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'minori',
+ name: 'ミノリ',
+ giftRatings: {
+ '栄養満載の総合ビタミンゼリー': 'ss',
+ 'ストリートオブヤンキー1巻': 'sa',
+ '『銃 可愛い 青春』': 'na',
+ '百科事典': 'na',
+ '禁断の愛~許されないからこそ美しく~': 'nb',
+ 'ゲームマガジン「ヒットガールズ」': 'nb',
+ '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'nb',
+ '古典の詩集': 'nb',
+ 'ザ・サプリメント': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'mimori',
+ name: 'ミモリ',
+ giftRatings: {
+ 'おしゃれなくし': 'sa',
+ '裁縫キット': 'sa',
+ '可愛い食器セット': 'na',
+ '刺繍付きのハンカチ': 'na',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'mimori_swimsuit',
+ name: 'ミモリ(水着)',
+ giftRatings: {
+ '裁縫キット': 'ss',
+ 'ストリートオブヤンキー1巻': 'sa',
+ 'サミュエラ「ザ・ビヨンド」': 'sa',
+ '古典の詩集': 'na',
+ '夏模様の浮き輪': 'na',
+ '禁断の愛~許されないからこそ美しく~': 'nb',
+ 'ゲームマガジン「ヒットガールズ」': 'nb',
+ 'チェリーローズカラーのグロス': 'nb',
+ 'お肌を透明にするBBクリーム': 'nb',
+ 'ミリタリー用カモフラージュクリーム3種セット': 'nb',
+ '『銃 可愛い 青春』': 'nb',
+ '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'nb',
+ '可愛い食器セット': 'nb',
+ '百科事典': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'miyako',
+ name: 'ミヤコ',
+ giftRatings: {
+ 'エイ~ブックレア': 'ss',
+ '『銃 可愛い 青春』': 'na',
+ 'ミリタリー用カモフラージュクリーム3種セット': 'nb',
+ 'MX-レーションC型デザート風味': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'miyako_swimsuit',
+ name: 'ミヤコ(水着)',
+ giftRatings: {
+ 'レースの枕': 'ss',
+ '『銃 可愛い 青春』': 'na',
+ '夏模様の浮き輪': 'na',
+ 'ミリタリー用カモフラージュクリーム3種セット': 'nb',
+ 'MX-レーションC型デザート風味': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'miyu',
+ name: 'ミユ',
+ giftRatings: {
+ 'おしゃれなくし': 'ss',
+ '刺繍付きのハンカチ': 'na',
+ 'ミリタリー用カモフラージュクリーム3種セット': 'nb',
+ 'MX-レーションC型デザート風味': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'miyu_swimsuit',
+ name: 'ミユ(水着)',
+ giftRatings: {
+ 'おしゃれなくし': 'ss',
+ '刺繍付きのハンカチ': 'na',
+ '夏模様の浮き輪': 'na',
+ 'ミリタリー用カモフラージュクリーム3種セット': 'nb',
+ 'MX-レーションC型デザート風味': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'miyo',
+ name: 'ミヨ',
+ giftRatings: {
+ 'ストリートオブヤンキー1巻': 'ss',
+ 'レトロな卵の工芸品': 'sa',
+ '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'na',
+ '禁断の愛~許されないからこそ美しく~': 'nb',
+ 'ゲームマガジン「ヒットガールズ」': 'nb',
+ '『銃 可愛い 青春』': 'nb',
+ '埋蔵金の地図': 'nb',
+ '世界で最も無駄な絡繰りボックス': 'nb',
+ '古典の詩集': 'nb',
+ '高級そうな欲望のつぼ': 'nb',
+ 'ぜんまい式オルゴール': 'nb',
+ '百科事典': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'mutsuki',
+ name: 'ムツキ',
+ giftRatings: {
+ 'ボードゲーム「ザ・人生」': 'sa',
+ 'コスプレ用ぐるぐるメガネ': 'ns',
+ '天体望遠鏡': 'nb',
+ 'リボンのついた熊のぬいぐるみ': 'nb',
+ '30色の絵の具セット': 'nb',
+ '頭脳開発キューブパズル': 'nb',
+ '食虫植物の植木鉢': 'nb',
+ '高級そうな欲望のつぼ': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'mutsuki_newyear',
+ name: 'ムツキ(正月)',
+ giftRatings: {
+ 'ボードゲーム「ザ・人生」': 'sa',
+ 'コスプレ用ぐるぐるメガネ': 'na',
+ '天体望遠鏡': 'nb',
+ 'リボンのついた熊のぬいぐるみ': 'nb',
+ '30色の絵の具セット': 'nb',
+ '頭脳開発キューブパズル': 'nb',
+ '食虫植物の植木鉢': 'nb',
+ '高級そうな欲望のつぼ': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'ムツキ_ドレス',
+ name: 'ムツキ(ドレス)',
+ giftRatings: {
+ 'ボードゲーム「ザ・人生」': 'ss',
+ 'コスプレ用ぐるぐるメガネ': 'na',
+ '天体望遠鏡': 'nb',
+ 'リボンのついた熊のぬいぐるみ': 'nb',
+ '30色の絵の具セット': 'nb',
+ '頭脳開発キューブパズル': 'nb',
+ '食虫植物の植木鉢': 'nb',
+ '高級そうな欲望のつぼ': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'megu',
+ name: 'メグ',
+ giftRatings: {
+ 'ボードゲーム「ザ・人生」': 'ss',
+ 'コスプレ用ぐるぐるメガネ': 'na',
+ '天体望遠鏡': 'nb',
+ 'リボンのついた熊のぬいぐるみ': 'nb',
+ '30色の絵の具セット': 'nb',
+ '頭脳開発キューブパズル': 'nb',
+ '食虫植物の植木鉢': 'nb',
+ '高級そうな欲望のつぼ': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'meru',
+ name: 'メル',
+ giftRatings: {
+ 'ボードゲーム「ザ・人生」': 'ss',
+ 'ストリートオブヤンキー1巻': 'sa',
+ '30色の絵の具セット': 'na',
+ '禁断の愛~許されないからこそ美しく~': 'nb',
+ 'ゲームマガジン「ヒットガールズ」': 'nb',
+ '『銃 可愛い 青春』': 'nb',
+ '天体望遠鏡': 'nb',
+ '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'nb',
+ 'コスプレ用ぐるぐるメガネ': 'nb',
+ 'リボンのついた熊のぬいぐるみ': 'nb',
+ '古典の詩集': 'nb',
+ '頭脳開発キューブパズル': 'nb',
+ '百科事典': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'moe',
+ name: 'モエ',
+ giftRatings: {
+ 'エイ~ブックレア': 'sa',
+ 'エーポッドプロ': 'na',
+ 'ミリタリー用カモフラージュクリーム3種セット': 'nb',
+ 'ゲームガールカラー復刻版': 'nb',
+ 'MX-レーションC型デザート風味': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'moe_swimsuit',
+ name: 'モエ(水着)',
+ giftRatings: {
+ 'エイ~ブックレア': 'ss',
+ '夏模様の浮き輪': 'na',
+ 'エーポッドプロ': 'nb',
+ 'ミリタリー用カモフラージュクリーム3種セット': 'nb',
+ 'ゲームガールカラー復刻版': 'nb',
+ 'MX-レーションC型デザート風味': 'nb',
+ 'コスプレ用ぐるぐるメガネ': 'nb',
+ '食虫植物の植木鉢': 'nb',
+ '高級そうな欲望のつぼ': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'momiji',
+ name: 'モミジ',
+ giftRatings: {
+ 'ストリートオブヤンキー1巻': 'ss',
+ '禁断の愛~許されないからこそ美しく~': 'na',
+ 'ゲームマガジン「ヒットガールズ」': 'nb',
+ 'チェリーローズカラーのグロス': 'nb',
+ '『銃 可愛い 青春』': 'nb',
+ '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'nb',
+ '古典の詩集': 'nb',
+ '百科事典': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'momoi',
+ name: 'モモイ',
+ giftRatings: {
+ 'ボードゲーム「ザ・人生」': 'ss',
+ 'ゲームガールカラー復刻版': 'na',
+ 'ゲームマガジン「ヒットガールズ」': 'nb',
+ 'O-フィット': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'momoi_maid',
+ name: 'モモイ(メイド)',
+ giftRatings: {
+ 'ボードゲーム「ザ・人生」': 'ss',
+ 'レースの枕': 'sa',
+ 'ゲームガールカラー復刻版': 'na',
+ 'ウェーブキャットの枕': 'nb',
+ 'ゲームマガジン「ヒットガールズ」': 'nb',
+ 'ゼリーズの枕': 'nb',
+ 'O-フィット': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'yakumo',
+ name: 'ヤクモ',
+ giftRatings: {
+ 'レトロな卵の工芸品': 'ss',
+ 'ストリートオブヤンキー1巻': 'sa',
+ '高級そうな欲望のつぼ': 'na',
+ '禁断の愛~許されないからこそ美しく~': 'nb',
+ 'ゲームマガジン「ヒットガールズ」': 'nb',
+ '『銃 可愛い 青春』': 'nb',
+ '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'nb',
+ '埋蔵金の地図': 'nb',
+ '世界で最も無駄な絡繰りボックス': 'nb',
+ '古典の詩集': 'nb',
+ 'ぜんまい式オルゴール': 'nb',
+ '百科事典': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'yuuka',
+ name: 'ユウカ',
+ giftRatings: {
+ 'エイ~ブックレア': 'ss',
+ '栄養満載の総合ビタミンゼリー': 'ss',
+ '頭脳開発キューブパズル': 'na',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'yuuka_sportswear',
+ name: 'ユウカ(体操服)',
+ giftRatings: {
+ '頭脳開発キューブパズル': 'na',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'yuuka_pajama',
+ name: 'ユウカ(パジャマ)',
+ giftRatings: {
+ 'レースの枕': 'ss',
+ 'サミュエラ「ザ・ビヨンド」': 'sa',
+ 'エイ~ブックレア': 'sa',
+ 'ウェーブキャットの枕': 'na',
+ 'エーポッドプロ': 'nb',
+ 'チェリーローズカラーのグロス': 'nb',
+ 'お肌を透明にするBBクリーム': 'nb',
+ 'ミリタリー用カモフラージュクリーム3種セット': 'nb',
+ 'ゲームガールカラー復刻版': 'nb',
+ 'ゼリーズの枕': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'yukari',
+ name: 'ユカリ',
+ giftRatings: {
+ 'おしゃれなくし': 'ss',
+ 'ミルフィーユの正統派パフェ': 'sa',
+ '刺繍付きのハンカチ': 'na',
+ '高級なクッキーセット': 'nb',
+ 'MX-レーションC型デザート風味': 'nb',
+ '抹茶味の瓶ラムネ': 'nb',
+ '大きなホールケーキ': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'yukari_swimsuit',
+ name: 'ユカリ(水着)',
+ giftRatings: {
+ 'おしゃれなくし': 'ss',
+ 'ミルフィーユの正統派パフェ': 'sa',
+ '刺繍付きのハンカチ': 'na',
+ '夏模様の浮き輪': 'na',
+ '高級なクッキーセット': 'nb',
+ 'MX-レーションC型デザート風味': 'nb',
+ '抹茶味の瓶ラムネ': 'nb',
+ '大きなホールケーキ': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'yuzu',
+ name: 'ユズ',
+ giftRatings: {
+ 'エイ~ブックレア': 'sa',
+ 'ゲームガールカラー復刻版': 'na',
+ 'エーポッドプロ': 'nb',
+ 'ゲームマガジン「ヒットガールズ」': 'nb',
+ 'O-フィット': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'yuzu_maid',
+ name: 'ユズ(メイド)',
+ giftRatings: {
+ 'エイ~ブックレア': 'ss',
+ 'ゲームガールカラー復刻版': 'na',
+ 'エーポッドプロ': 'nb',
+ 'ゲームマガジン「ヒットガールズ」': 'nb',
+ 'O-フィット': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'yuzu_battle',
+ name: 'ユズ(臨戦)',
+ giftRatings: {
+ 'エイ~ブックレア': 'ss',
+ 'サミュエラ「ザ・ビヨンド」': 'sa',
+ 'ミリタリー用カモフラージュクリーム3種セット': 'na',
+ 'ゲームガールカラー復刻版': 'na',
+ 'チェリーローズカラーのグロス': 'nb',
+ 'お肌を透明にするBBクリーム': 'nb',
+ 'MX-レーションC型デザート風味': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'yoshimi',
+ name: 'ヨシミ',
+ giftRatings: {
+ 'ミルフィーユの正統派パフェ': 'sa',
+ '高級なクッキーセット': 'na',
+ 'MX-レーションC型デザート風味': 'nb',
+ '抹茶味の瓶ラムネ': 'nb',
+ '大きなホールケーキ': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'yoshimi_band',
+ name: 'ヨシミ(バンド)',
+ giftRatings: {
+ 'ミルフィーユの正統派パフェ': 'ss',
+ '高級なクッキーセット': 'na',
+ '大きなホールケーキ': 'na',
+ 'MX-レーションC型デザート風味': 'nb',
+ '抹茶味の瓶ラムネ': 'nb',
+ 'チェリーローズカラーのグロス': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'rabu',
+ name: 'ラブ',
+ giftRatings: {
+ '1日3回ダンベルセット': 'ss',
+ 'レトロな卵の工芸品': 'sa',
+ 'ミルフィーユの正統派パフェ': 'sa',
+ 'ペロロの腹筋ローラー': 'na',
+ '高級なクッキーセット': 'nb',
+ 'MX-レーションC型デザート風味': 'nb',
+ '抹茶味の瓶ラムネ': 'nb',
+ 'O-フィット': 'nb',
+ '埋蔵金の地図': 'nb',
+ '世界で最も無駄な絡繰りボックス': 'nb',
+ '大きなホールケーキ': 'nb',
+ '高級そうな欲望のつぼ': 'nb',
+ '百科事典': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'rio',
+ name: 'リオ',
+ giftRatings: {
+ 'エイ~ブックレア': 'ss',
+ '天体望遠鏡': 'na',
+ 'エーポッドプロ': 'nb',
+ 'ゲームガールカラー復刻版': 'nb',
+ 'コスプレ用ぐるぐるメガネ': 'nb',
+ '食虫植物の植木鉢': 'nb',
+ '高級そうな欲望のつぼ': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'rio_battle',
+ name: 'リオ(臨戦)',
+ giftRatings: {
+ 'エイ~ブックレア': 'ss',
+ '天体望遠鏡': 'na',
+ 'エーポッドプロ': 'nb',
+ 'ゲームガールカラー復刻版': 'nb',
+ 'コスプレ用ぐるぐるメガネ': 'nb',
+ '世界で最も無駄な絡繰りボックス': 'nb',
+ '食虫植物の植木鉢': 'nb',
+ '高級そうな欲望のつぼ': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'ritsu',
+ name: 'リツ',
+ giftRatings: {
+ '最高級の楓の盆栽': 'ss',
+ 'ボードゲーム「ザ・人生」': 'sa',
+ 'コスプレ用ぐるぐるメガネ': 'na',
+ '天体望遠鏡': 'nb',
+ 'リボンのついた熊のぬいぐるみ': 'nb',
+ '30色の絵の具セット': 'nb',
+ '頭脳開発キューブパズル': 'nb',
+ '食虫植物の植木鉢': 'nb',
+ '高級そうな欲望のつぼ': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'rumi',
+ name: 'ルミ',
+ giftRatings: {
+ 'おしゃれなくし': 'ss',
+ '可愛い食器セット': 'na',
+ '刺繍付きのハンカチ': 'na',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'rei',
+ name: 'レイ',
+ giftRatings: {
+ 'エイ~ブックレア': 'ss',
+ 'エーポッドプロ': 'nb',
+ 'ゲームガールカラー復刻版': 'nb',
+ '埋蔵金の地図': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'reisa',
+ name: 'レイサ',
+ giftRatings: {
+ 'ミルフィーユの正統派パフェ': 'ss',
+ 'エイ~ブックレア': 'sa',
+ 'エーポッドプロ': 'na',
+ '高級なクッキーセット': 'nb',
+ 'ゲームガールカラー復刻版': 'nb',
+ 'MX-レーションC型デザート風味': 'nb',
+ '抹茶味の瓶ラムネ': 'nb',
+ '大きなホールケーキ': 'nb',
+ 'ぜんまい式オルゴール': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'reisa_magical',
+ name: 'レイサ(マジカル)',
+ giftRatings: {
+ 'ストリートオブヤンキー1巻': 'ss',
+ 'ミルフィーユの正統派パフェ': 'sa',
+ 'ボードゲーム「ザ・人生」': 'sa',
+ 'ゲームマガジン「ヒットガールズ」': 'na',
+ '禁断の愛~許されないからこそ美しく~': 'nb',
+ '高級なクッキーセット': 'nb',
+ '『銃 可愛い 青春』': 'nb',
+ '天体望遠鏡': 'nb',
+ 'ゲームガールカラー復刻版': 'nb',
+ 'MX-レーションC型デザート風味': 'nb',
+ '抹茶味の瓶ラムネ': 'nb',
+ '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'nb',
+ 'O-フィット': 'nb',
+ 'コスプレ用ぐるぐるメガネ': 'nb',
+ 'リボンのついた熊のぬいぐるみ': 'nb',
+ '30色の絵の具セット': 'nb',
+ '古典の詩集': 'nb',
+ '頭脳開発キューブパズル': 'nb',
+ '大きなホールケーキ': 'nb',
+ '百科事典': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'reijo',
+ name: 'レイジョ',
+ giftRatings: {
+ '1日3回ダンベルセット': 'ss',
+ 'ザ・サプリメント': 'na',
+ 'ペロロの腹筋ローラー': 'nb',
+ 'O-フィット': 'nb',
+ '埋蔵金の地図': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'rena',
+ name: 'レナ',
+ giftRatings: {
+ '裁縫キット': 'ss',
+ 'サミュエラ「ザ・ビヨンド」': 'sa',
+ 'おしゃれなくし': 'sa',
+ 'リボンのついた熊のぬいぐるみ': 'na',
+ '刺繍付きのハンカチ': 'na',
+ 'チェリーローズカラーのグロス': 'nb',
+ 'お肌を透明にするBBクリーム': 'nb',
+ 'ミリタリー用カモフラージュクリーム3種セット': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'renge',
+ name: 'レンゲ',
+ giftRatings: {
+ '裁縫キット': 'ss',
+ '音楽演奏会入場券': 'sa',
+ '禁断の愛~許されないからこそ美しく~': 'na',
+ '映画の前売りペアチケット': 'na',
+ '可愛い食器セット': 'na',
+ 'ハイクラスビュッフェ招待券': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'renge_swimsuit',
+ name: 'レンゲ(水着)',
+ giftRatings: {
+ '裁縫キット': 'ss',
+ 'ストリートオブヤンキー1巻': 'sa',
+ '禁断の愛~許されないからこそ美しく~': 'na',
+ '夏模様の浮き輪': 'na',
+ 'ゲームマガジン「ヒットガールズ」': 'nb',
+ '『銃 可愛い 青春』': 'nb',
+ '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'nb',
+ '古典の詩集': 'nb',
+ '可愛い食器セット': 'nb',
+ '百科事典': 'nb',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'wakamo',
+ name: 'ワカモ',
+ giftRatings: {
+ 'おしゃれなくし': 'ss',
+ 'リボンのついた熊のぬいぐるみ': 'na',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'wakamo_swimsuit',
+ name: 'ワカモ(水着)',
+ giftRatings: {
+ 'おしゃれなくし': 'ss',
+ 'リボンのついた熊のぬいぐるみ': 'na',
+ '夏模様の浮き輪': 'na',
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'hatsune_miku',
+ name: '初音ミク',
+ giftRatings: {
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'misaka_mikoto',
+ name: '御坂美琴',
+ giftRatings: {
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'shokuhou_misaki',
+ name: '食蜂操祈',
+ giftRatings: {
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+ {
+ id: 'saten_ruiko',
+ name: '佐天涙子',
+ giftRatings: {
+ } as Record<string, keyof typeof GIFT_EXP>
+ },
+] as const;
+
+export type CharacterId = typeof CHARACTER_LIST[number]['id'];
diff --git a/wiki2convert.py b/wiki2convert.py
@@ -0,0 +1,64 @@
+import re
+
+# 1. ここにWikiのテーブル行を貼り付けてください
+wiki_data = """
+
+"""
+
+def generate_ts_file(raw_text, filename="converted_characters.txt"):
+ # 列構成の定義 (インデックス, 終了インデックス, レーティング)
+ # 0列目は名前なので除外、1列目からカウント
+ col_definitions = [
+ (1, 2, 'ss'), # 高級贈り物 特大 (2列)
+ (3, 4, 'sa'), # 高級贈り物 大 (2列)
+ (5, 7, 'na'), # 通常贈り物 大 (3列)
+ (8, 15, 'nb') # 通常贈り物 中 (8列)
+ ]
+
+ lines = raw_text.strip().split('\n')
+ formatted_output = []
+
+ for line in lines:
+ if not line.strip() or line.startswith('|~') or 'h' in line.split('|')[-1]:
+ continue
+
+ # セルに分割
+ cells = [c.strip() for c in line.strip('|').split('|')]
+
+ # キャラクター名の抽出
+ name_match = re.search(r'\[\[([^\]]+)\]\]$', cells[0])
+ name = name_match.group(1) if name_match else "unknown"
+
+ # IDの生成 (適宜調整してください)
+ char_id = name.lower().replace('(', '_').replace(')', '')
+
+ # ギフトレーティングの解析
+ gift_ratings = []
+ for start, end, rating in col_definitions:
+ for i in range(start, end + 1):
+ if i < len(cells) and cells[i]:
+ # アイテム名の抽出
+ item_name = re.search(r',([^,)]+)\);', cells[i])
+ if item_name:
+ gift_ratings.append(f" '{item_name.group(1)}': '{rating}',")
+
+ # TypeScript形式の文字列組み立て
+ char_block = [
+ f" {{",
+ f" id: '{char_id}',",
+ f" name: '{name}',",
+ f" giftRatings: {{",
+ *gift_ratings,
+ f" }} as Record<string, keyof typeof GIFT_EXP>",
+ f" }},"
+ ]
+ formatted_output.append("\n".join(char_block))
+
+ # ファイル書き出し
+ with open(filename, "w", encoding="utf-8") as f:
+ f.write("\n".join(formatted_output))
+
+ print(f"成功: {filename} に書き出しました。")
+
+if __name__ == "__main__":
+ generate_ts_file(wiki_data)+
\ No newline at end of file