├── .babelrc
├── .eslintignore
├── .eslintrc.js
├── .github
└── workflows
│ ├── ci.yml
│ └── e2e.yml
├── .gitignore
├── .prettierignore
├── .prettierrc
├── Procfile
├── README.md
├── app.js
├── app.json
├── cypress.json
├── cypress
├── integration
│ └── smoke_spec.js
├── plugins
│ └── index.js
└── support
│ ├── commands.js
│ └── index.js
├── locales
├── en
│ └── common.json
├── ja
│ └── common.json
├── ko
│ └── common.json
└── zh
│ └── common.json
├── notes
├── herokupg.md
└── sql.md
├── package-lock.json
├── package.json
├── sitemap.xml
├── src
├── client.js
├── constants.js
├── jobs
│ ├── kasegi.js
│ ├── kasegi_new.js
│ ├── sitemap.js
│ └── sitemap.xml
├── modules
│ ├── fetch.js
│ ├── pg.js
│ └── query.js
├── public
│ ├── image
│ │ ├── ._2-1.jpg
│ │ ├── ._2-2.jpg
│ │ ├── ._addFriend.jpg
│ │ ├── ._sharedSongEdit.jpg
│ │ ├── 1-1.jpg
│ │ ├── 1-2.jpg
│ │ ├── 1-3.jpg
│ │ ├── 2-1.jpeg
│ │ ├── 2-1.jpg
│ │ ├── 2-2.jpeg
│ │ ├── 2-2.jpg
│ │ ├── 2-3.jpg
│ │ ├── addFriend.jpg
│ │ └── sharedSongEdit.jpg
│ ├── js
│ │ └── google_analytics.js
│ └── sitemap.xml
├── react
│ ├── App.jsx
│ ├── AppHeader.jsx
│ ├── ErrorListPage
│ │ ├── ErrorListPage.jsx
│ │ ├── ErrorListPageContainer.jsx
│ │ └── index.js
│ ├── IndexPage
│ │ ├── BookmarkletScript.jsx
│ │ ├── HowToUseSection.jsx
│ │ ├── IndexPage.jsx
│ │ ├── OtherSection.jsx
│ │ └── index.js
│ ├── KasegiNewPage
│ │ ├── KasegiTable.jsx
│ │ ├── component.jsx
│ │ ├── container.jsx
│ │ └── index.js
│ ├── KasegiPage
│ │ ├── KasegiIndexPage.jsx
│ │ ├── KasegiPage.jsx
│ │ ├── KasegiPageContainer.jsx
│ │ ├── KasegiTable.jsx
│ │ └── index.js
│ ├── ListPage
│ │ ├── ListPage.jsx
│ │ ├── ListPageContainer.jsx
│ │ └── index.js
│ ├── SharedSongsPage
│ │ ├── SearchBox.jsx
│ │ ├── SharedSongsPage.jsx
│ │ ├── SharedSongsPageContainer.jsx
│ │ └── index.js
│ ├── SkillPage
│ │ ├── SavedSkillPageContainer.jsx
│ │ ├── SkillPage.jsx
│ │ ├── SkillPageContainer.jsx
│ │ ├── SkillTable.jsx
│ │ └── index.js
│ ├── SlideToggle.jsx
│ ├── UserVoicePage.jsx
│ ├── styles
│ │ └── skillColor.js
│ ├── useLocalStorage.js
│ └── withMediaQuery.js
├── resolvers.js
├── schema.js
├── scripts
│ ├── uploaddata_core.js
│ ├── uploaddata_exchain.js
│ ├── uploaddata_exchain_dev.js
│ ├── uploaddata_exchain_local.js
│ ├── uploaddata_highvoltage.js
│ ├── uploaddata_highvoltage_dev.js
│ ├── uploaddata_highvoltage_local.js
│ ├── uploaddata_latest.js
│ ├── uploaddata_latest_dev.js
│ ├── uploaddata_latest_local.js
│ ├── uploaddata_matixx.js
│ ├── uploaddata_matixx_dev.js
│ ├── uploaddata_matixx_local.js
│ ├── uploaddata_nextage.js
│ ├── uploaddata_nextage_dev.js
│ ├── uploaddata_nextage_local.js
│ ├── uploaddata_tbre.js
│ ├── uploaddata_tbre_dev.js
│ ├── uploaddata_tbre_local.js
│ └── uploaddata_template.js
├── server.js
├── theme.js
└── views
│ └── htmlTemplate.js
├── webpack.client.config.js
├── webpack.scripts.config.js
└── webpack.server.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "@babel/preset-env",
4 | "@babel/preset-react"
5 | ],
6 | "plugins": [
7 | "@babel/plugin-transform-regenerator",
8 | "@babel/plugin-proposal-class-properties"
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | /node_modules/*
2 | /src/public/js/*.js
3 | app.dist.js
4 | /cypress
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: ["eslint:recommended", "plugin:react/recommended"],
3 | plugins: ["react"],
4 | rules: {
5 | "no-console": "off",
6 | "react/prop-types": "off"
7 | },
8 | overrides: [
9 | {
10 | files: ["src/react/**/*.jsx", "src/**/*.js", "app.js", "webpack.*.js"],
11 | parser: "babel-eslint",
12 | env: {
13 | browser: true,
14 | node: true,
15 | es6: true
16 | }
17 | },
18 | ]
19 | };
20 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | pull_request:
5 | types: [ opened, synchronize, reopened ]
6 |
7 | jobs:
8 | lint:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v2
12 | - name: Use Node.js
13 | uses: actions/setup-node@v1
14 | with:
15 | node-version: 16.15.0
16 | - run: npm install
17 | - run: |
18 | npm run lint
19 | npm run format:check
--------------------------------------------------------------------------------
/.github/workflows/e2e.yml:
--------------------------------------------------------------------------------
1 | name: E2E test
2 |
3 | on:
4 | push:
5 | branches:
6 | - develop
7 |
8 | jobs:
9 | E2E:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v2
13 | - name: Use Node.js
14 | uses: actions/setup-node@v1
15 | with:
16 | node-version: 16.15.0
17 | - run: npm install
18 | - run: npm install -g cypress@9
19 | - uses: ssdh233/wait-for-deployment-action@v3.0.1
20 | id: deployment
21 | with:
22 | github-token: ${{ github.token }}
23 | environment: gitadora-skill-viewer-dev
24 | timeout: 600
25 | interval: 60
26 | - run: cypress run
27 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules/*
2 | /.env
3 | /src/public/js/uploaddata_*
4 | /src/public/js/bundle*
5 | /src/public/js/client.js
6 | /src/public/js/server.js
7 | /src/public/js/manifest.json
8 |
9 | /cypress/videos
10 | /cypress/screenshots
11 |
12 | app.dist.js
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | /node_modules/*
2 | /src/public/js/*.js
3 | app.dist.js
4 | /cypress
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | tabWidth: 2
2 | printWidth: 120
3 |
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | web: ./node_modules/.bin/babel-node app.js
2 | web-win: .\node_modules\.bin\babel-node app.js
3 | dev: ./node_modules/.bin/nodemon --signal SIGINT app.dist.js --exec ./node_modules/.bin/babel-node
4 | dev-win: .\node_modules\.bin\nodemon --signal SIGINT app.dist.js --exec .\node_modules\.bin\babel-node
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | http://gsv.fun
2 |
3 | ## About
4 |
5 | A web site which uses bookmarklet to fetch and store GITADORA skill data from official site, to share with other players.
6 |
7 | ## Contribute
8 |
9 | If you want to contribute to this repository, feel free to contact me from [my blog](http://ssdh233.me/gsv/) :)
10 |
--------------------------------------------------------------------------------
/app.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const app = express();
3 | const path = require("path");
4 | const fs = require("fs");
5 | const bodyParser = require("body-parser");
6 | const cookieParser = require("cookie-parser");
7 | const compression = require("compression");
8 | const CronJob = require("cron").CronJob;
9 | const { ApolloServer } = require("apollo-server-express");
10 |
11 | const reactRoute = require("./src/server").default;
12 | const typeDefs = require("./src/schema");
13 | const resolvers = require("./src/resolvers");
14 |
15 | app.use(compression());
16 |
17 | app.use(
18 | express.static("src/public", {
19 | maxAge: 31536000000,
20 | setHeaders: (res, path) => {
21 | if (path.includes("uploaddata")) {
22 | res.set("Cache-Control", "no-cache");
23 | }
24 | }
25 | })
26 | );
27 | app.set("views", path.join(__dirname, "/src/views"));
28 |
29 | app.use(function(req, res, next) {
30 | res.header("Access-Control-Allow-Origin", "*");
31 | res.header(
32 | "Access-Control-Allow-Headers",
33 | "Origin, X-Requested-With, Content-Type, Accept"
34 | );
35 | next();
36 | });
37 |
38 | app.use(bodyParser());
39 | app.use(cookieParser());
40 |
41 | // for graphql
42 | const server = new ApolloServer({
43 | typeDefs,
44 | resolvers,
45 | introspection: true
46 | });
47 | server.applyMiddleware({ app });
48 |
49 | // for react pages
50 | app.get("/:locale(en|ja|zh|ko)", reactRoute);
51 | app.get("/:locale(en|ja|zh|ko)/*", reactRoute);
52 |
53 | // redirect if the first param is not language
54 | app.get("*", (req, res) => {
55 | const lang =
56 | req.cookies.locale || req.acceptsLanguages("ja", "zh", "en", "ko") || "ja";
57 |
58 | res.setHeader("Cache-Control", "public, max-age=0");
59 | if (req.url === "/") {
60 | res.redirect(301, `/${lang}`);
61 | } else {
62 | res.redirect(301, `/${lang}${req.url}`);
63 | }
64 | });
65 |
66 | // for jobs
67 | fs.readdirSync("./src/jobs").forEach(file => {
68 | if (file.substr(-3) == ".js") {
69 | let job = require(`./src/jobs/${file}`);
70 |
71 | if (job.job && job.cronSchedule) {
72 | const cronJob = new CronJob(job.cronSchedule, job.job);
73 | cronJob.start();
74 | console.log("Starting cron job", file);
75 |
76 | if (process.env.RUN_CRON_ON_START_UP === "true") {
77 | console.log("Run cron job once on deployment:", file);
78 | job.job();
79 | }
80 | }
81 | }
82 | });
83 |
84 | app.listen(process.env.PORT, function() {
85 | console.log(`This app listening on port ${process.env.PORT}!`);
86 | });
87 |
88 | process.on("uncaughtException", function(err) {
89 | console.log("uncaughtException => ", err);
90 | });
91 |
92 | process.on("SIGINT", () => {
93 | console.log("Recieved signal SIGINT");
94 | process.exit(0);
95 | });
96 |
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gitadora-skill-viewer",
3 | "scripts": {
4 | },
5 | "env": {
6 | "MY_DATABASE_URL": {
7 | "required": true
8 | },
9 | "NODE_ENV": {
10 | "required": true
11 | },
12 | "NPM_CONFIG_PRODUCTION": {
13 | "required": true
14 | }
15 | },
16 | "formation": {
17 | "web": {
18 | "quantity": 1
19 | }
20 | },
21 | "addons": [
22 | "heroku-postgresql"
23 | ],
24 | "buildpacks": [
25 | {
26 | "url": "heroku/nodejs"
27 | }
28 | ]
29 | }
30 |
--------------------------------------------------------------------------------
/cypress.json:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/cypress/integration/smoke_spec.js:
--------------------------------------------------------------------------------
1 | const SITE_URL = "http://gitadora-skill-viewer-dev.herokuapp.com/ja";
2 | // const SITE_URL = "http://localhost:5000"; // for local testing
3 | const { CURRENT_VERSION } = require("../../src/constants");
4 |
5 | describe("Smoke test", function() {
6 | it("Visits home page", function() {
7 | cy.visit(SITE_URL);
8 | });
9 |
10 | it("Visits skill list page", function() {
11 | cy.visit(`${SITE_URL}/${CURRENT_VERSION}/list`);
12 |
13 | cy.get("#list-table").find(".rt-tbody > .rt-tr-group").its("length").should("eq", 100);
14 | });
15 |
16 | it("Visits kasegi page", function() {
17 | cy.visit(`${SITE_URL}/${CURRENT_VERSION}/kasegi/d/6000`);
18 |
19 | cy.get("#kasegi-hot-table").find(".rt-tbody > .rt-tr-group").its("length").should("eq", 25);
20 | cy.get("#kasegi-other-table").find(".rt-tbody > .rt-tr-group").its("length").should("eq", 25);
21 | });
22 |
23 | it("Visits skill page", function() {
24 | cy.visit(`${SITE_URL}/exchain/1/d`);
25 | cy.get("#skill-table-hot").find("tbody > tr").its("length").should("eq", 25);
26 | cy.get("#skill-table-other").find("tbody > tr").its("length").should("eq", 25);
27 | });
28 | });
--------------------------------------------------------------------------------
/cypress/plugins/index.js:
--------------------------------------------------------------------------------
1 | ///
2 | // ***********************************************************
3 | // This example plugins/index.js can be used to load plugins
4 | //
5 | // You can change the location of this file or turn off loading
6 | // the plugins file with the 'pluginsFile' configuration option.
7 | //
8 | // You can read more here:
9 | // https://on.cypress.io/plugins-guide
10 | // ***********************************************************
11 |
12 | // This function is called when a project is opened or re-opened (e.g. due to
13 | // the project's config changing)
14 |
15 | /**
16 | * @type {Cypress.PluginConfig}
17 | */
18 | module.exports = (on, config) => {
19 | // `on` is used to hook into various events Cypress emits
20 | // `config` is the resolved Cypress config
21 | }
22 |
--------------------------------------------------------------------------------
/cypress/support/commands.js:
--------------------------------------------------------------------------------
1 | // ***********************************************
2 | // This example commands.js shows you how to
3 | // create various custom commands and overwrite
4 | // existing commands.
5 | //
6 | // For more comprehensive examples of custom
7 | // commands please read more here:
8 | // https://on.cypress.io/custom-commands
9 | // ***********************************************
10 | //
11 | //
12 | // -- This is a parent command --
13 | // Cypress.Commands.add("login", (email, password) => { ... })
14 | //
15 | //
16 | // -- This is a child command --
17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
18 | //
19 | //
20 | // -- This is a dual command --
21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
22 | //
23 | //
24 | // -- This will overwrite an existing command --
25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
26 |
--------------------------------------------------------------------------------
/cypress/support/index.js:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example support/index.js is processed and
3 | // loaded automatically before your test files.
4 | //
5 | // This is a great place to put global configuration and
6 | // behavior that modifies Cypress.
7 | //
8 | // You can change the location of this file or turn off
9 | // automatically serving support files with the
10 | // 'supportFile' configuration option.
11 | //
12 | // You can read more here:
13 | // https://on.cypress.io/configuration
14 | // ***********************************************************
15 |
16 | // Import commands.js using ES2015 syntax:
17 | import './commands'
18 |
19 | // Alternatively you can use CommonJS syntax:
20 | // require('./commands')
21 |
--------------------------------------------------------------------------------
/locales/en/common.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Gitadora Skill Viewer - Share your GITADORA skill list",
3 | "home": "home",
4 | "kasegiSong": "Song ranking",
5 | "lang": "Lang",
6 | "intro": {
7 | "title": "Introdution",
8 | "desc": "A web site which uses bookmarklet to fetch and store skill data from GITADORA official site, to share with other players.",
9 | "info": {
10 | "title": "Notice",
11 | "content0": "Added support for GALAXY WAVE DELTA.",
12 | "content2": "I created a twitter account for gsv.fun. Please follow me :)"
13 | }
14 | },
15 | "how": {
16 | "title": "How to use",
17 | "upload": {
18 | "title": "Upload skill data",
19 | "step1": {
20 | "desc": "1.Add the below script to bookmark",
21 | "remark": "※Some browsers(e.g. safari、chrome on mobile)don't provide the feature of creating a new bookmark. In that case you can add a bookmark for any site first, then edit it and replace the URL address with the below script.",
22 | "imgDesc1": "The bookmark name doesn't matter. Put whatever you like.",
23 | "imgDesc2": "Paste the script on the URL input",
24 | "currentVersion": "For current version: ",
25 | "currentVersionDesc": "(This script is always getting data from the latest version. No need to update the script when next version comes)",
26 | "oldVersion": "For old version:"
27 | },
28 | "step2": {
29 | "desc": "2.Log in to GITADORA official site, and click the added bookmark",
30 | "imgDesc1": "Make sure you are on Konami official site",
31 | "imgDesc2": "After login, click the bookmark which you just added",
32 | "androidWorkaround": "If nothing happens after clicking the bookmark, your device might not support executing javascripts from bookmark (it usually happens on Android devices). If so, you could try123",
33 | "androidWorkaroundStep1": "Copy the script above",
34 | "androidWorkaroundStep2": "Go to GITADORA official site, then paste the script into address bar",
35 | "androidWorkaroundStep3": "After pasting, add javascript:
to the beginning of address bar ?",
36 | "androidWorkaroundStep4": "Press enter to execute the script",
37 | "androidWorkaroundDesc": "Most of the browsers would remove javascript:
while pasting to address bar, so you will need to add it back again. Skip this step if javascript:
still exsits."
38 | },
39 | "step3": {
40 | "desc": "3.Wait for 5 to 10 seconds. It will jump to your skill page after finishing uploading. Use this URL to share your skill data with others.",
41 | "imgDesc1": "This url is publicly accessible.
Share it with others!"
42 | }
43 | }
44 | },
45 | "desc": {
46 | "title": "Readme",
47 | "1st": "1.It's not hard to upload faked skill data to gsv.fun if someone wants. So gsv.fun can not guarantee the authenticity of skill data here, and also it can not guarantee that your uploaded skill data won't be falsified by others, unless you saved your data.",
48 | "2nd": "2.Different with the commonly used skill simulator, on this web site a user can not change his uploaded skill data."
49 | },
50 | "other": {
51 | "title": "Other",
52 | "code": {
53 | "title": "Source code"
54 | },
55 | "voice": {
56 | "title": "Feedback",
57 | "desc1": "If you find a bug or have some suggestions, please feel free to leave a message on User Voice page :)"
58 | },
59 | "browser": {
60 | "title": "Recommended browser"
61 | },
62 | "oldLinks": {
63 | "title": "Old Links",
64 | "oldList": "Player list(old versions)"
65 | }
66 | },
67 | "snackbar": {
68 | "message": "Go to {name}'s skill page?",
69 | "yes": "yes",
70 | "no": "no"
71 | },
72 | "uservoice": {
73 | "message": "If you find a bug or have some suggestions, please feel free to leave a message here :)"
74 | },
75 | "list": {
76 | "title": "Player list",
77 | "searchPlaceholder": "Search by player name"
78 | },
79 | "kasegi": {
80 | "title": "song ranking",
81 | "songname": "Name",
82 | "level": "Level",
83 | "percent": "%",
84 | "averageskill": "Average",
85 | "averageplayerskill": "Player Average",
86 | "compare": "Compare",
87 | "compareSkill": "{name}'s skill",
88 | "compareTitle": "Comparing with {compareSkill}",
89 | "scope": {
90 | "3000": "Green",
91 | "3500": "Green",
92 | "4000": "Blue",
93 | "4500": "Blue",
94 | "5000": "Purple",
95 | "5500": "Purple",
96 | "6000": "Red",
97 | "6500": "Red",
98 | "7000": "Bronze",
99 | "7500": "Silver",
100 | "8000": "Gold",
101 | "8500": "Rainbow",
102 | "9000": "Rainbow"
103 | },
104 | "old": "song ranking(divided by 500)"
105 | },
106 | "skill": {
107 | "title": "{name}'s skill table",
108 | "aboutGsv": "About gsv.fun",
109 | "compareWithKasegi": "Compare with {scope} ranking",
110 | "saveSkill": "save current skill table",
111 | "alreadySaved": "This skill table has already been saved.",
112 | "savedList": "Skill history",
113 | "latestSkill": "Check {name}'s latest skill",
114 | "comparingWith": "Comparing with {something}... ",
115 | "rivalSkill": "{name}'s skill'({point})",
116 | "cancel": "Cancel",
117 | "compareWithPlayer": "Compare with my skill({name})",
118 | "compareWith1": "Compare with",
119 | "compareWith2": " ",
120 | "skillId": "skill id",
121 | "skillIdDesc": "skill id is the number on the URL of your skill sheet. For example, the skill id of http://gsv.fun/ja/exchain/1234/d is 1234.",
122 | "skillIdOk": "OK",
123 | "songName": "SONG",
124 | "level": "LEVEL",
125 | "achieve": "ACHIEVE",
126 | "skillPoint": "SKILL"
127 | },
128 | "sharedSongs": {
129 | "title": "Song sharing",
130 | "desc": "This page provides a platform to let players search unlocked songs which are shared through 'recommend songs to friends' by other players.",
131 | "noResults": "No results. Maybe you could try to ask for the song on the below forum.",
132 | "forum": "forum",
133 | "forumDesc": "Leave a message for the song you didn't find :)",
134 | "howToUse": {
135 | "title": "How to use",
136 | "step1": "Select guitar/drum first, then search song by name",
137 | "step2": "On official site's フレンド編集 page, select guitar or drum first, then use Gitadora ID search the player who has the song you want, and add him as a friend",
138 | "step3": "Then you can play these unlocked songs at game center! However, you can only get 3 songs randomly (every play) from another player unless you two are mutual friends, so we recommend you add multiple friends for one song if you really really want that song."
139 | },
140 | "howToShare": {
141 | "title": "How to share your song",
142 | "desc1": "Edit on official site's 『フレンドにオススメ』編集 page, and upload skill as usal, no extra steps!",
143 | "desc2": "(About how to upload skill, please refer to 'How to use' section on home page.",
144 | "ps1": "According to Konami official, you can only get 3 songs randomly (every play) from another player unless you two are mutual friends.",
145 | "ps2": "Notice that you can only share songs for GuitarFreaks and Drummania respectively."
146 | }
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/locales/ja/common.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Gitadora Skill Viewer - ギタドラのスキル帳を共有するウェブサイト",
3 | "home": "ホーム",
4 | "kasegiSong": "稼ぎ曲",
5 | "lang": "言語",
6 | "intro": {
7 | "title": "なにこれ?",
8 | "desc": "ギタドラの公式サイトからスキルデータを取得し、スキルの進捗を記録したり、スキル表を他のプレイヤーにシェアしたりするためのウェブサイトです。",
9 | "info": {
10 | "title": "お知らせ",
11 | "content0": "GALAXY WAVE DELTAに対応しました。",
12 | "content2": "gsv.funのツイッターアカウントを作りました。フォローしていただけると嬉しいです。"
13 | }
14 | },
15 | "how": {
16 | "title": "使い方",
17 | "upload": {
18 | "title": "スキルデータのアップロード",
19 | "step1": {
20 | "desc": "1.ブックマークに下記のスクリプトを登録",
21 | "remark": "一部のブラウザ(Safari、スマホのChromeとか)ではブックマークの新規作成ができませんが、
- とりあえず任意のウェブサイトをブックマークに登録
- 登録したブックマークを編集
- URLアドレスのところに、登録したURLを消してスクリプトを入れる
の手順でもスクリプトをブックマークに登録することができます。",
22 | "imgDesc1": "名前は適当でいい",
23 | "imgDesc2": "URLのところにスクリプトをペーストして",
24 | "currentVersion": "現行バージョン",
25 | "currentVersionDesc": "(このスクリプトは常に現行バージョンに対応しています。新しいバージョンになってもスクリプトを更新する必要がありません。)",
26 | "oldVersion": "古いバージョン"
27 | },
28 | "step2": {
29 | "desc": "2.コナミ様のサイトにログインした状態で、登録されたブックマークをクリック",
30 | "imgDesc1": "コナミ様のサイトにいることを確認",
31 | "imgDesc2": "ログインした状態でブックマークをクリック",
32 | "androidWorkaround": "ブックマークをクリックして反応がなかった場合、お持ちのデバイスはブックマークでのスクリプト実行をサポートしていない可能性があります(特にAndroid機種が多い)。その場合は123という方法もあります。(画像による説明 thanks@beit_soldier)",
33 | "androidWorkaroundStep1": "上のスクリプトをコピーする",
34 | "androidWorkaroundStep2": "ギタドラの公式サイトにアクセスし、上のスクリプトをペーストする",
35 | "androidWorkaroundStep3": "javascript:
を先頭に追加する?",
36 | "androidWorkaroundStep4": "エンターキーを押してスクリプトを実行する",
37 | "androidWorkaroundDesc": "ほとんどのブラウザはペーストする時に先頭のjavascript:
を消しますため、ペースト後手動で追加し直す必要があります。もしペースト後すでに先頭にjavascript:
があった場合、このステップは要りません。"
38 | },
39 | "step3": {
40 | "desc": "3.データのアップロードが終わったらすぐ(約5~10秒)スキル表示ページへの遷移が行われます。遷移先のURLを控えてください",
41 | "imgDesc1": "共有用URLです"
42 | }
43 | }
44 | },
45 | "desc": {
46 | "title": "仕様説明",
47 | "1st": "1.ある程度の専門知識を持っているならスキルデータを改竄することが可能です。このサイトに載せられているデータが全部真実であること、かつ、アップロードされたデータが改竄されないことについて保証できかねます。",
48 | "2nd": "2.このウェブサイトは皆さん昔良く使用していたスキルシミュレーターと違いまして、取得したスキルデータを編集する機能を提供していません。"
49 | },
50 | "other": {
51 | "title": "その他",
52 | "code": {
53 | "title": "ソースコード"
54 | },
55 | "voice": {
56 | "title": "不具合報告、ご意見など",
57 | "desc1": "不具合の報告や、新しい機能の追加のご要望がありましたらUser Voiceにコメントしていただければ助かります。"
58 | },
59 | "browser": {
60 | "title": "推奨ブラウザ"
61 | },
62 | "oldLinks": {
63 | "title": "古いリンク",
64 | "oldList": "過去バージョンのユーザ一覧"
65 | }
66 | },
67 | "snackbar": {
68 | "message": "スキルページ({name})に行きますか",
69 | "yes": "はい",
70 | "no": "いいえ"
71 | },
72 | "uservoice": {
73 | "message": "不具合の報告や、新しい機能の追加のご要望がありましたらコメントしていただければ助かります。"
74 | },
75 | "list": {
76 | "title": "ユーザー一覧",
77 | "searchPlaceholder": "プレイヤー名で検索"
78 | },
79 | "kasegi": {
80 | "title": "稼ぎ曲ランキング",
81 | "desc": "ギタドラ{type}{scope}の稼ぎ情報を載せております。",
82 | "songname": "曲名",
83 | "level": "レベル",
84 | "percent": "登録率",
85 | "averageskill": "平均",
86 | "averageplayerskill": "プレイヤー",
87 | "compare": "比較",
88 | "compareSkill": "{name}さんのスキル",
89 | "compareTitle": "{compareSkill}と比較中",
90 | "scope": {
91 | "3000": "緑ネ",
92 | "3500": "緑グラ",
93 | "4000": "青ネ",
94 | "4500": "青グラ",
95 | "5000": "紫ネ",
96 | "5500": "紫グラ",
97 | "6000": "赤ネ",
98 | "6500": "赤グラ",
99 | "7000": "銅ネ",
100 | "7500": "銀ネ",
101 | "8000": "金ネ",
102 | "8500": "虹ネ",
103 | "9000": "虹ネ"
104 | },
105 | "old": "稼ぎ曲リスト(500単位区切り)"
106 | },
107 | "skill": {
108 | "title": "{name}さんの{type}スキル",
109 | "aboutGsv": "gsv.funについて",
110 | "compareWithKasegi": "{scope}の稼ぎ曲と比較",
111 | "saveSkill": "スキルをセーブする",
112 | "alreadySaved": "このスキル表はもうセーブされていますよ",
113 | "savedList": "スキル履歴",
114 | "latestSkill": "{name}さんの最新のスキル表",
115 | "comparingWith": "{something}と比較中...",
116 | "rivalSkill": "{name}のスキル({point})",
117 | "cancel": "取り消し",
118 | "compareWithPlayer": "自分({name})のスキルと比較",
119 | "compareWith1": " ",
120 | "compareWith2": "のスキルと比較",
121 | "skillId": "スキルID",
122 | "skillIdDesc": "スキルIDとは、スキル帳のURL(例えばhttp://gsv.fun/ja/exchain/1234/d)上の数字の1234のことです。",
123 | "skillIdOk": "わかった",
124 | "songName": "曲名",
125 | "level": "レベル",
126 | "achieve": "達成率",
127 | "skillPoint": "スキル"
128 | },
129 | "sharedSongs": {
130 | "title": "解禁曲シェア",
131 | "desc": "ギタドラ公式が提供する未解禁の曲の救済手段としての「フレンドにオススメ」機能をサポートする機能です。",
132 | "noResults": "見つかりませんでした。下のコミュ版でリクエストしてみよう!",
133 | "forum": "コミュ版",
134 | "forumDesc": "検索できなかった未解禁の曲の共有のリクエストとかで使ってください〜",
135 | "howToUse": {
136 | "title": "使い方",
137 | "step1": "機種を選んで、曲名で未解禁の楽曲を検索をする",
138 | "step2": "公式サイトのフレンド編集で、ギタドラIDで曲の持ち主を検索して、フレンドに追加する",
139 | "step3": "あとはゲーセンに行くだけ!ただし、非相互登録のフレンドの共有は毎回のプレイで、5曲の中ランダムに3曲が選出されるので、どうしてもやりたい曲があったら複数のフレンドを登録することをお薦めです。"
140 | },
141 | "howToShare": {
142 | "title": "曲を共有するには?",
143 | "desc1": "公式サイトの『フレンドにオススメ』編集で曲を登録して、スキルをアップロードするだけでOK!",
144 | "desc2": "(スキルをアップロードはホームページの使い方に参照)",
145 | "ps1": "ただし、公式の仕様によると、非相互登録の共有は毎回のプレイで、5曲中に3曲がランダムに選出されます。",
146 | "ps2": "あとギターとドラムの共有曲が別々になっていることを注意してください。"
147 | }
148 | }
149 | }
--------------------------------------------------------------------------------
/locales/ko/common.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Gitadora Skill Viewer - 기타도라 스킬 표 공유 사이트",
3 | "home": "홈",
4 | "kasegiSong": "스킬 곡 랭킹",
5 | "lang": "언어",
6 | "intro": {
7 | "title": "어떤 사이트인가요?",
8 | "desc": "기타도라 공식 사이트에서 스킬 데이터를 받아오거나, 스킬의 변화를 기록하거나, 자신의 스킬 표를 다른 사람에게 공유하는 사이트입니다.",
9 | "info": {
10 | "title": "공지사항",
11 | "content0": "Added support for GALAXY WAVE DELTA.",
12 | "content2": "gsv.fun 사이트의 트위터 계정을 개설했습니다. 꼭 팔로우해주세요"
13 | }
14 | },
15 | "how": {
16 | "title": "사용법",
17 | "upload": {
18 | "title": "스킬 데이터 업로드 하기",
19 | "step1": {
20 | "desc": "1. 아래의 스크립트를 북마크에 추가하세요.",
21 | "remark": "※일부 브라우저(예: 사파리, 크롬 모바일)는 북마크를 직접 새로 만드는 기능을 제공하지 않습니다. 그런 경우에는 아무 사이트나 북마크에 등록한 뒤, 해당 북마크의 URL을 아래 스크립트로 바꾸시면 됩니다.",
22 | "imgDesc1": "북마크의 이름은 원하시는 이름으로 넣으세요",
23 | "imgDesc2": "URL란에 스크립트를 붙여넣으세요",
24 | "currentVersion": "현행 버전용 스크립트:",
25 | "currentVersionDesc": "(이 스크립트는 항상 현행 버전으로부터 스킬 데이터를 가져옵니다. 버전이 바뀌었을 때 별도로 북마크를 수정하지 않아도 됩니다.)",
26 | "oldVersion": "구 버전용 스크립트:"
27 | },
28 | "step2": {
29 | "desc": "2. 기타도라 공식 사이트에 로그인 하신 뒤, 1번에서 추가한 북마크를 클릭해 주세요.",
30 | "imgDesc1": "코나미 공식 사이트인지 꼭 확인하세요",
31 | "imgDesc2": "로그인 하신 뒤, 추가한 북마크를 클릭하세요",
32 | "androidWorkaround": "If nothing happens after clicking the bookmark, your device might not support executing javascripts from bookmark (it usually happens on Android devices). If so, you could try123",
33 | "androidWorkaroundStep1": "Copy the script above",
34 | "androidWorkaroundStep2": "Go to GITADORA official site, then paste the script into address bar",
35 | "androidWorkaroundStep3": "After pasting, add javascript:
to the beginning of address bar ?",
36 | "androidWorkaroundStep4": "Press enter to execute the script",
37 | "androidWorkaroundDesc": "Most of the browsers would remove javascript:
while pasting to address bar, so you will need to add it back again. Skip this step if javascript:
still exsits."
38 | },
39 | "step3": {
40 | "desc": "3. 5~10초 정도 기다리시면, 업로드가 끝난 후 자동으로 스킬 페이지로 이동합니다. 이 URL을 통해 다른 사람과 스킬 표를 공유하세요.",
41 | "imgDesc1": "다른 사람에게 이 링크를 공유하세요!"
42 | }
43 | }
44 | },
45 | "desc": {
46 | "title": "주의사항",
47 | "1st": "1. 어느 정도의 컴퓨터 지식만 있으면 gsv.fun에 가짜 스킬 데이터를 올릴 수 있습니다. 그렇기에 gsv.fun에 올라온 스킬 데이터의 무결성을 보장할 수 없음을 명심해 주세요.",
48 | "2nd": "2. 많은 사람들이 이용하는 타 사이트와 달리, 직접 데이터를 수동으로 입력하는 기능을 제공하지 않습니다."
49 | },
50 | "other": {
51 | "title": "그 외",
52 | "code": {
53 | "title": "소스 코드"
54 | },
55 | "voice": {
56 | "title": "피드백",
57 | "desc1": "버그를 찾으셨거나 새로운 기능에 대한 의견이 있으시다면, User Voice 페이지에 코멘트를 남겨주시면 감사하겠습니다."
58 | },
59 | "browser": {
60 | "title": "추천 브라우저"
61 | },
62 | "oldLinks": {
63 | "title": "Old Links",
64 | "oldList": "Player list(old versions)"
65 | }
66 | },
67 | "snackbar": {
68 | "message": "{name}님의 스킬 페이지로 이동할까요?",
69 | "yes": "예",
70 | "no": "아니오"
71 | },
72 | "uservoice": {
73 | "message": "버그를 찾으셨거나 새로운 기능에 대한 의견이 있으시다면, 코멘트를 남겨주시면 감사하겠습니다."
74 | },
75 | "list": {
76 | "title": "유저 일람",
77 | "searchPlaceholder": "Search by player name"
78 | },
79 | "kasegi": {
80 | "title": " 스킬 곡 랭킹 |",
81 | "songname": "곡명",
82 | "level": "레벨",
83 | "percent": "스킬 인 비율",
84 | "averageskill": "평균 스킬",
85 | "averageplayerskill": "플레이어 평균",
86 | "compare": "비교",
87 | "compareSkill": "{name}님의 스킬",
88 | "compareTitle": "{compareSkill}과 비교",
89 | "scope": {
90 | "3000": "초록",
91 | "3500": "초록 그라데이션",
92 | "4000": "파랑",
93 | "4500": "파랑 그라데이션",
94 | "5000": "보라",
95 | "5500": "보라 그라데이션",
96 | "6000": "빨강",
97 | "6500": "빨강 그라데이션",
98 | "7000": "동색",
99 | "7500": "은색",
100 | "8000": "금색",
101 | "8500": "무지개",
102 | "9000": "무지개"
103 | },
104 | "old": "song ranking(divided by 500)"
105 | },
106 | "skill": {
107 | "title": "{name}님의 {type} 스킬 표",
108 | "aboutGsv": "gsv.fun에 대해서",
109 | "compareWithKasegi": "{scope}대에서 주로 스킬 인 하는 곡과 비교",
110 | "saveSkill": "save current skill table",
111 | "alreadySaved": "This skill table has already been saved.",
112 | "savedList": "스킬 이력",
113 | "latestSkill": "{name}님의 최근 스킬 표",
114 | "comparingWith": "{something}와 비교 중...",
115 | "rivalSkill": "{name}님의 스킬 표({point})",
116 | "cancel": "돌아가기",
117 | "compareWithPlayer": "자신({name})의 스킬 표와 비교",
118 | "compareWith1": " ",
119 | "compareWith2": "의 스킬 표와 비교",
120 | "skillId": "스킬 ID",
121 | "skillIdDesc": "스킬 ID는 스킬 표 URL 상의 숫자(예를 들어 http://gsv.fun/ko/highvoltage/1234/d 의 1234)를 말하는 것입니다.",
122 | "skillIdOk": "확인",
123 | "songName": "곡",
124 | "level": "레벨",
125 | "achieve": "달성률",
126 | "skillPoint": "스킬"
127 | },
128 | "sharedSongs": {
129 | "title": "곡 공유",
130 | "desc": "'프렌드에게 추천(フレンドにオススメ)' 기능을 통해 자신이 해금한 곡을 공유하고, 해금하지 못한 곡을 공유 받을 수 있는 기능입니다.",
131 | "noResults": "찾을 수 없습니다. 아래의 게시판에서 곡을 공유해줄 수 있는 사람을 찾아보세요.",
132 | "forum": "게시판",
133 | "forumDesc": "이곳에서 곡을 공유해줄 수 있는 사람을 찾아보세요.",
134 | "howToUse": {
135 | "title": "사용법",
136 | "step1": "기타/드럼 중 어느 쪽인지 고른 뒤, 이름으로 곡을 검색하세요.",
137 | "step2": "기타도라 공식 사이트의 フレンド編集 페이지에서, 기타/드럼 중 어느 쪽인지 고른 뒤, 기타도라 ID를 통해서 해당 곡을 공유하고 있는 사람을 검색해 프렌드로 추가하세요.",
138 | "step3": "이제 오락실에서 공유받은 미해금 곡을 플레이할 수 있습니다! 하지만 한 쪽에서만 프렌드로 등록되어 있는 경우 공유 중인 5개의 곡 중 무작위로 3곡만 선택되므로, 한 곡을 매번 플레이하고 싶다면 그 곡을 갖고 있는 사람 여럿을 프렌드로 추가하는 것을 권장합니다."
139 | },
140 | "howToShare": {
141 | "title": "곡을 공유하는 법",
142 | "desc1": "기타도라 공식 사이트의 『フレンドにオススメ』編集 페이지에서 곡을 등록한 뒤, 평소대로 스킬 데이터를 업로드하시면 자동으로 이 페이지에 정보가 업데이트됩니다.",
143 | "desc2": "(스킬 데이터를 업로드하는 방법은, 홈페이지의 사용법을 참고해주세요)",
144 | "ps1": "공식적으로 서로 프렌드로 등록하지 않는 한 플레이마다 랜덤으로 5곡 중 3곡만 공유받게 됩니다.",
145 | "ps2": "드럼과 기타는 별도로 공유곡을 지정하기 때문에, 한쪽에서 공유 중이더라도 다른 쪽에서는 공유 중이지 않을 수 있습니다."
146 | }
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/locales/zh/common.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Gitadora Skill Viewer - 分享你的Skill",
3 | "home": "主页",
4 | "kasegiSong": "赚分曲排行",
5 | "lang": "语言",
6 | "intro": {
7 | "title": "这是什么?",
8 | "desc": "一个从konami的官方网站上获取skill数据,用来保存、分享的网站。",
9 | "info": {
10 | "title": "通知",
11 | "content0": "对应了新版本GALAXY WAVE DELTA。",
12 | "content2": "最近建了一个gsv.fun用的twitter 账号,欢迎互fo!"
13 | }
14 | },
15 | "how": {
16 | "title": "使用方法",
17 | "upload": {
18 | "title": "上传skill data",
19 | "step1": {
20 | "desc": "1.在浏览器的书签上登录下面的代码",
21 | "remark": "※有些浏览器(比如Safari、手机上的Chrome)上没有办法新建书签,可以先随便添加一个任意网站的书签,然后选择编辑,把URL地址栏的内容换成下面的代码就好了。",
22 | "imgDesc1": "书签的名字可以随便填",
23 | "imgDesc2": "把代码粘贴到URL这里",
24 | "currentVersion": "当前版本",
25 | "currentVersionDesc": "(这个代码一直对应最新版本,即使换代也不用来更新代码)",
26 | "oldVersion": "旧版本"
27 | },
28 | "step2": {
29 | "desc": "2.在konami的网站上保持登录状态的时候,点击刚才登录的书签",
30 | "imgDesc1": "确认在Konami的网站上",
31 | "imgDesc2": "登录后点击这个书签",
32 | "androidWorkaround": "如果点击书签没有反应,有可能是设备不支持执行书签的代码(安卓设备这种情况比较多),这时可以尝试123",
33 | "androidWorkaroundStep1": "复制上面的代码",
34 | "androidWorkaroundStep2": "去Gitadora的官方主页,并且把上面的代码拷贝到地址栏",
35 | "androidWorkaroundStep3": "拷贝后,在地址栏的开头添加javascript:
?",
36 | "androidWorkaroundStep4": "按下确定/Enter键执行代码",
37 | "androidWorkaroundDesc": "大部分浏览器会在拷贝时删除掉前面的javascript:
,所以需要在拷贝后手动重新添加。如果拷贝后前面的javascript:
还在的话,请略过这一步。"
38 | },
39 | "step3": {
40 | "desc": "3.等待5~10秒钟之后,就会自动跳转到skill data的表示画面。请记住跳转之后的地址(用来公开当前的skill)",
41 | "imgDesc1": "这个URL可以用来分享当前的skill data"
42 | }
43 | }
44 | },
45 | "desc": {
46 | "title": "一些说明",
47 | "1st": "1.有一定程度的知识的话,完全可以篡改skill data。所以没有办法保证在这个网站上的data都是真实的,也没有办法保证上传过的data不被别人篡改(除非使用了保存skill data功能)",
48 | "2nd": "2.这个网站和之前大家常用的skill simulator不同,没有手动更改skill的功能。"
49 | },
50 | "other": {
51 | "title": "其他",
52 | "code": {
53 | "title": "Source code"
54 | },
55 | "voice": {
56 | "title": "各种意见和建议",
57 | "desc1": "有什么意见,或者bug反馈的话欢迎在User Voice里留言!"
58 | },
59 | "browser": {
60 | "title": "推荐浏览器"
61 | },
62 | "oldLinks": {
63 | "title": "旧链接存放处",
64 | "oldList": "旧版本的玩家列表"
65 | }
66 | },
67 | "snackbar": {
68 | "message": "要去{name}的skill表页面吗",
69 | "yes": "好的",
70 | "no": "不用"
71 | },
72 | "uservoice": {
73 | "message": "有什么意见,或者bug反馈的话欢迎在这留言!"
74 | },
75 | "list": {
76 | "title": "玩家列表",
77 | "searchPlaceholder": "搜索玩家名"
78 | },
79 | "kasegi": {
80 | "title": "赚分曲排行",
81 | "songname": "曲名",
82 | "level": "level",
83 | "percent": "%",
84 | "averageskill": "平均",
85 | "averageplayerskill": "玩家平均",
86 | "compare": "比较",
87 | "compareSkill": "{name}的skill表",
88 | "compareTitle": "正在与{compareSkill}进行比较",
89 | "scope": {
90 | "3000": "绿名",
91 | "3500": "绿名",
92 | "4000": "蓝名",
93 | "4500": "蓝名",
94 | "5000": "紫名",
95 | "5500": "紫名",
96 | "6000": "红名",
97 | "6500": "红名",
98 | "7000": "铜名",
99 | "7500": "银名",
100 | "8000": "金名",
101 | "8500": "虹名",
102 | "9000": "虹名"
103 | },
104 | "old": "赚分曲排行(500分区间)"
105 | },
106 | "skill": {
107 | "title": "{name}的{type} skill表",
108 | "aboutGsv": "关于gsv.fun",
109 | "compareWithKasegi": "和{scope}的赚分曲比较",
110 | "saveSkill": "保存当前skill表",
111 | "alreadySaved": "这个skill表已经被保存过了。",
112 | "savedList": "过去的记录",
113 | "latestSkill": "去看{name}最新的skill表",
114 | "compareWithPlayer": "和自己({name})比较",
115 | "comparingWith": "正在与{something}进行比较...",
116 | "rivalSkill": "{name}的skill表({point})",
117 | "cancel": "取消",
118 | "compareWith1": "和",
119 | "compareWith2": "比较",
120 | "skillId": "skill id",
121 | "skillIdDesc": "skill id是skill表的URL上的数字. 比如http://gsv.fun/ja/exchain/1234/d的skill id是1234.",
122 | "skillIdOk": "OK",
123 | "songName": "曲名",
124 | "level": "level",
125 | "achieve": "达成率",
126 | "skillPoint": "skill"
127 | },
128 | "sharedSongs": {
129 | "title": "解禁曲共享",
130 | "desc": "本页面为gitadora官方的「向朋友推荐歌曲」功能提供了一个搜索平台",
131 | "noResults": "没有找到这首歌。试试在下面留言求歌吧~",
132 | "forum": "留言板",
133 | "forumDesc": "这里主要用来求歌之类的",
134 | "howToUse": {
135 | "title": "使用方法",
136 | "step1": "先选择机种,然后用曲名搜索想要的歌曲",
137 | "step2": "在官方网站的フレンド編集页面,用gitadora id搜索有这首歌的玩家,添加为好友",
138 | "step3": "然后去机厅打就可以了!但是注意没有互相加好友的话只能随机分享其中三首歌,所以有可能要多试几把才能出来~(如果有特别想玩的歌,建议加多个好友增加出现几率)"
139 | },
140 | "howToShare": {
141 | "title": "共享歌曲的方法",
142 | "desc1": "在官方网站的『フレンドにオススメ』編集页面添加好歌曲,然后像以前一样上传skill就OK啦!",
143 | "desc2": "(关于如何上传skill,请参考本站首页的使用方法一栏)",
144 | "ps1": "根据官方的规定,没有相互加好友的话只能随机分享三首歌。",
145 | "ps2": "请注意吉他和鼓的分享歌曲是分开来算的。"
146 | }
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/notes/herokupg.md:
--------------------------------------------------------------------------------
1 | ### dump
2 | ```
3 | heroku pg:backups:capture --app gitadora-skill-viewer
4 | heroku pg:backups:download --app gitadora-skill-viewer
5 | ```
6 |
7 | ### restore
8 | 1. upload it to somewhere
9 | For example, https://www.jsdelivr.com/?docs=gh
10 | https://cdn.jsdelivr.net/gh/matsumatsu233/MyStorage/latest.dump
11 |
12 | 2.
13 | ```
14 | heroku pg:backups:restore '' MY_DATABASE_URL --app gitadora-skill-viewer
15 | ```
16 |
17 | ### backup schedule
18 |
19 | ```
20 | heroku pg:backups:schedule ONYX --at "04:00 Asia/Tokyo" --app gitadora-skill-viewer
21 | ```
22 |
--------------------------------------------------------------------------------
/notes/sql.md:
--------------------------------------------------------------------------------
1 | ### add count column for kasegi table
2 | ```sql
3 | ALTER TABLE kasegi ADD COLUMN count int;
4 | ```
5 |
6 | ### sharedSongs table bugfix
7 | ```sql
8 | alter table shared_songs drop constraint shared_songs_pkey;
9 | alter table shared_songs add constraint shared_songs_pkey PRIMARY KEY ("songName", "type");
10 | ```
11 |
12 | ### add errorReports table
13 |
14 | ```sql
15 | CREATE TABLE errorReports(
16 | "version" varchar(50),
17 | "error" text,
18 | "date" varchar(20),
19 | "userAgent" text
20 | );
21 | ```
22 |
23 |
24 | ### add sharedSongs column
25 | ```sql
26 | ALTER TABLE skill
27 | ADD COLUMN "sharedSongs" json
28 |
29 | CREATE TABLE shared_songs(
30 | "songName" text,
31 | "version" varchar(50),
32 | "type" varchar(1),
33 | "count" integer,
34 | PRIMARY KEY ("songName")
35 | );
36 | ```
37 |
38 | ### migration
39 |
40 | #### skill
41 | ```sql
42 | CREATE TABLE skill(
43 | "playerId" serial,
44 | "version" varchar(50),
45 | "cardNumber" varchar(16),
46 | "gitadoraId" varchar(20),
47 | "playerName" varchar(20),
48 | "guitarSkillPoint" varchar(10),
49 | "drumSkillPoint" varchar(10),
50 | "guitarSkill" json,
51 | "drumSkill" json,
52 | "updateDate" varchar(20),
53 | "updateCount" integer,
54 | PRIMARY KEY ("playerId", "version")
55 | );
56 |
57 | INSERT INTO skill ("playerId", "version", "cardNumber", "playerName", "guitarSkillPoint", "drumSkillPoint", "guitarSkill", "drumSkill", "updateDate", "updateCount") (
58 | SELECT
59 | id,
60 | 'tb' AS version,
61 | card_number,
62 | player_name,
63 | ((guitar_skill::json -> 'hot' ->> 'point')::float + (guitar_skill::json -> 'other' ->> 'point')::float)::text,
64 | ((drum_skill::json -> 'hot' ->> 'point')::float + (drum_skill::json -> 'other' ->> 'point')::float)::text,
65 | guitar_skill::json,
66 | drum_skill::json,
67 | update_date,
68 | update_count
69 | FROM
70 | skill_tb
71 | UNION ALL
72 | SELECT
73 | id,
74 | 'tbre' AS version,
75 | card_number,
76 | player_name,
77 | ((guitar_skill::json -> 'hot' ->> 'point')::float + (guitar_skill::json -> 'other' ->> 'point')::float)::text,
78 | ((drum_skill::json -> 'hot' ->> 'point')::float + (drum_skill::json -> 'other' ->> 'point')::float)::text,
79 | guitar_skill::json,
80 | drum_skill::json,
81 | update_date,
82 | update_count
83 | FROM
84 | skill_tbre
85 | UNION ALL
86 | SELECT
87 | id,
88 | 'matixx' AS version,
89 | card_number,
90 | player_name,
91 | ((guitar_skill::json -> 'hot' ->> 'point')::float + (guitar_skill::json -> 'other' ->> 'point')::float)::text,
92 | ((drum_skill::json -> 'hot' ->> 'point')::float + (drum_skill::json -> 'other' ->> 'point')::float)::text,
93 | guitar_skill::json,
94 | drum_skill::json,
95 | update_date,
96 | update_count
97 | FROM
98 | skill_matixx
99 | UNION ALL
100 | SELECT
101 | id,
102 | 'exchain' AS version,
103 | card_number,
104 | player_name,
105 | ((guitar_skill::json -> 'hot' ->> 'point')::float + (guitar_skill::json -> 'other' ->> 'point')::float)::text,
106 | ((drum_skill::json -> 'hot' ->> 'point')::float + (drum_skill::json -> 'other' ->> 'point')::float)::text,
107 | guitar_skill::json,
108 | drum_skill::json,
109 | update_date,
110 | update_count
111 | FROM
112 | skill_exchain);
113 | ```
114 |
115 | #### skillp
116 | ```sql
117 | CREATE TABLE skillp(
118 | "skillId" serial,
119 | "version" varchar(50),
120 | "playerId" serial,
121 | "playerName" varchar(20),
122 | "type" varchar(1),
123 | "skillPoint" varchar(10),
124 | "skill" json,
125 | "updateDate" varchar(20),
126 | PRIMARY KEY ("skillId", "version")
127 | );
128 |
129 | SELECT id, count(*)
130 | FROM skillp_exchain
131 | GROUP BY id
132 | HAVING count(*) > 1;
133 |
134 | DELETE FROM skillp_exchain
135 | WHERE ctid IN (SELECT ctid FROM skillp_exchain WHERE id=1038 LIMIT 1);
136 |
137 | INSERT INTO skillp
138 | SELECT
139 | id,
140 | 'tb' AS version,
141 | skill_id,
142 | player_name,
143 | CASE TYPE
144 | WHEN 'drum' THEN
145 | 'd'
146 | WHEN 'guitar' THEN
147 | 'g'
148 | END,
149 | skill,
150 | skill_data::json,
151 | update_date
152 | FROM
153 | skillp_tb
154 | UNION ALL
155 | SELECT
156 | id,
157 | 'tbre' AS version,
158 | skill_id,
159 | player_name,
160 | CASE TYPE
161 | WHEN 'drum' THEN
162 | 'd'
163 | WHEN 'guitar' THEN
164 | 'g'
165 | END,
166 | skill,
167 | skill_data::json,
168 | update_date
169 | FROM
170 | skillp_tbre
171 | UNION ALL
172 | SELECT
173 | id,
174 | 'matixx' AS version,
175 | skill_id,
176 | player_name,
177 | CASE TYPE
178 | WHEN 'drum' THEN
179 | 'd'
180 | WHEN 'guitar' THEN
181 | 'g'
182 | END,
183 | skill,
184 | skill_data::json,
185 | update_date
186 | FROM
187 | skillp_matixx
188 | UNION ALL
189 | SELECT
190 | id,
191 | 'exchain' AS version,
192 | skill_id,
193 | player_name,
194 | CASE TYPE
195 | WHEN 'drum' THEN
196 | 'd'
197 | WHEN 'guitar' THEN
198 | 'g'
199 | END,
200 | skill,
201 | skill_data::json,
202 | update_date
203 | FROM
204 | skillp_exchain;
205 | ```
206 |
207 | ### kasegi
208 |
209 | ```sql
210 | create table kasegi(version varchar(50) NOT NULL, type varchar(50) NOT NULL,scope integer NOT NULL, list_hot text, list_other text, count integer);
211 | ```
212 |
213 | ### kasegi (250)
214 |
215 | ```sql
216 | create table kasegi_new(version varchar(50) NOT NULL, type varchar(50) NOT NULL,scope integer NOT NULL, list_hot text, list_other text, count integer);
217 | ```
218 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gitadora-skill-viewer",
3 | "description": "",
4 | "engines": {
5 | "node": "16.15.0"
6 | },
7 | "main": "app.js",
8 | "scripts": {
9 | "heroku-postbuild": "npm run build:prod && npm run build:scripts:prod",
10 | "build:client": "webpack-dev-server --config webpack.client.config.js",
11 | "build:server": "webpack --watch --config webpack.server.config.js",
12 | "build": "npm-run-all --parallel build:client build:server",
13 | "build:prod": "webpack -p --config webpack.server.config.js && webpack -p --config webpack.client.config.js",
14 | "build:scripts": "webpack --config webpack.scripts.config.js --watch --mode=development",
15 | "build:scripts:prod": "webpack --config webpack.scripts.config.js",
16 | "lint": "eslint . --ext .js --ext .jsx",
17 | "format": "prettier --write src//**/*.{js,jsx}",
18 | "format:check": "prettier --check src//**/*.{js,jsx}"
19 | },
20 | "author": "",
21 | "license": "ISC",
22 | "dependencies": {
23 | "@apollo/react-hooks": "^3.0.1",
24 | "@apollo/react-ssr": "^3.0.1",
25 | "@material-ui/core": "^4.3.2",
26 | "@material-ui/icons": "^4.2.1",
27 | "@material-ui/lab": "^4.0.0-alpha.61",
28 | "@material-ui/styles": "^4.3.0",
29 | "@sentry/react": "^6.8.0",
30 | "@sentry/tracing": "^6.8.0",
31 | "apollo-cache-inmemory": "^1.6.3",
32 | "apollo-client": "^2.6.4",
33 | "apollo-link-http": "^1.5.15",
34 | "apollo-server-express": "^2.14.2",
35 | "body-parser": "^1.15.2",
36 | "compression": "^1.7.1",
37 | "compression-webpack-plugin": "^1.0.1",
38 | "cookie-parser": "^1.4.3",
39 | "cron": "^1.5.0",
40 | "dotenv": "^16.4.3",
41 | "downshift": "^3.2.12",
42 | "es6-promise": "^4.2.5",
43 | "express": "^4.17.3",
44 | "flat": "^5.0.1",
45 | "graphql": "^14.4.2",
46 | "graphql-tag": "^2.10.1",
47 | "isomorphic-fetch": "^2.2.1",
48 | "jquery": "^3.5.0",
49 | "node-fetch": "^2.6.7",
50 | "pg": "^8.8.0",
51 | "pug": "^3.0.1",
52 | "react": "^16.9.0",
53 | "react-dom": "^16.9.0",
54 | "react-helmet": "^5.2.1",
55 | "react-intl": "^3.1.12",
56 | "react-lazyload": "^2.6.2",
57 | "react-router": "^5.0.1",
58 | "react-router-dom": "^5.0.1",
59 | "react-table": "6.8.6",
60 | "styled-components": "^4.3.2",
61 | "url-search-params": "^1.1.0",
62 | "webpack-dev-server": "^3.11.3",
63 | "xml-js": "^1.6.8",
64 | "xmldom": "^0.6.0"
65 | },
66 | "devDependencies": {
67 | "@babel/cli": "^7.1.2",
68 | "@babel/core": "^7.1.2",
69 | "@babel/node": "^7.0.0",
70 | "@babel/plugin-proposal-class-properties": "^7.0.0",
71 | "@babel/polyfill": "^7.0.0",
72 | "@babel/preset-env": "^7.1.0",
73 | "@babel/preset-react": "^7.0.0",
74 | "babel-core": "^7.0.0-bridge.0",
75 | "babel-eslint": "^9.0.0",
76 | "babel-loader": "^8.0.4",
77 | "css-loader": "^6.7.2",
78 | "eslint": "^5.6.0",
79 | "eslint-plugin-react": "^7.34.0",
80 | "kill-port": "^1.3.2",
81 | "node-sass": "^9.0.0",
82 | "nodemon": "^2.0.20",
83 | "npm-run-all": "^4.1.5",
84 | "prettier": "1.19.1",
85 | "sass-loader": "^7.1.0",
86 | "style-loader": "^0.23.0",
87 | "webpack": "^4.47.0",
88 | "webpack-cli": "^3.1.2",
89 | "webpack-manifest-plugin": "^2.0.4",
90 | "webpack-node-externals": "^3.0.0"
91 | },
92 | "nodemonConfig": {
93 | "ignore": [
94 | "src/react/*"
95 | ],
96 | "events": {
97 | "restart": "./node_modules/.bin/kill-port 5000",
98 | "crash": "./node_modules/.bin/kill-port 5000"
99 | }
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/src/client.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | // eslint-disable-next-line react/no-deprecated
3 | import { hydrate } from "react-dom";
4 | import { BrowserRouter } from "react-router-dom";
5 | import { IntlProvider } from "react-intl";
6 | import { ApolloProvider } from "@apollo/react-hooks";
7 | import { ApolloClient } from "apollo-client";
8 | import { InMemoryCache } from "apollo-cache-inmemory";
9 | import { HttpLink } from "apollo-link-http";
10 | import * as Sentry from "@sentry/react";
11 | import { Integrations } from "@sentry/tracing";
12 |
13 | import App from "./react/App.jsx";
14 |
15 | Sentry.init({
16 | dsn: "https://6d26c7e100a84ae2ae01f54719c5ca19@o912155.ingest.sentry.io/5848955",
17 | integrations: [new Integrations.BrowserTracing()],
18 |
19 | // Set tracesSampleRate to 1.0 to capture 100%
20 | // of transactions for performance monitoring.
21 | // We recommend adjusting this value in production
22 | tracesSampleRate: 0.05,
23 | environment: process.env.NODE_ENV,
24 | });
25 |
26 | const { locale, messages, initialThemeKey } = JSON.parse(window.App);
27 |
28 | const link = new HttpLink({
29 | uri: "/graphql"
30 | });
31 |
32 | const client = new ApolloClient({
33 | cache: new InMemoryCache().restore(window.__APOLLO_STATE__),
34 | link,
35 | ssrForceFetchDelay: 100
36 | });
37 |
38 | hydrate(
39 |
40 |
41 |
42 |
43 |
44 |
45 | ,
46 | document.getElementById("app")
47 | );
48 |
--------------------------------------------------------------------------------
/src/constants.js:
--------------------------------------------------------------------------------
1 | const APP_VERSION = "v1.41.3";
2 |
3 | const ALL_VERSIONS = ["galaxywave", "galaxywave_delta", "fuzzup", "highvoltage", "nextage", "exchain", "matixx", "tbre", "tb"];
4 |
5 | const CURRENT_VERSION = ALL_VERSIONS[0];
6 | const CURRENT_VERSION_2 = ALL_VERSIONS[1];
7 |
8 | const ON_SERVICE_VERSIONS = ["galaxywave", "galaxywave_delta", "fuzzup", "highvoltage"];
9 |
10 | // which has a different path with newer versions (no /eam)
11 | const NO_EAM_PATH_VERSIONS = ["nextage", "exchain", "matixx", "tbre", "tb"];
12 |
13 | const VERSION_NAME = {
14 | tb: "Tri-Boost",
15 | tbre: "Tri-Boost Re:EVOLVE",
16 | matixx: "Matixx",
17 | exchain: "EXCHAIN",
18 | nextage: "NEX+AGE",
19 | highvoltage: "HIGH-VOLTAGE",
20 | fuzzup: "FUZZ-UP",
21 | galaxywave: "GALAXY WAVE",
22 | galaxywave_delta: "GALAXY WAVE DELTA"
23 | };
24 |
25 | const OLD_NAME_MAP = {
26 | "Windy Fairy -GITADO ROCK ver.-": "Windy Fairy -GITADOROCK ver.-"
27 | };
28 |
29 | module.exports = {
30 | APP_VERSION,
31 | CURRENT_VERSION,
32 | CURRENT_VERSION_2,
33 | ON_SERVICE_VERSIONS,
34 | NO_EAM_PATH_VERSIONS,
35 | ALL_VERSIONS,
36 | VERSION_NAME,
37 | OLD_NAME_MAP
38 | };
39 |
--------------------------------------------------------------------------------
/src/jobs/kasegi.js:
--------------------------------------------------------------------------------
1 | const { CURRENT_VERSION } = require("../constants");
2 | const pg = require("../modules/pg");
3 |
4 | function isValidSkillData(skillData) {
5 | return (
6 | skillData.hot &&
7 | skillData.hot.data &&
8 | skillData.hot.data.length >= 25 &&
9 | skillData.other &&
10 | skillData.other.data &&
11 | skillData.other.data.length >= 25
12 | );
13 | }
14 |
15 | function gatherKasegiResult({ skillData, kasegiResult }) {
16 | const newKasegiResult = Object.assign({}, kasegiResult);
17 | const skillPoint = (parseFloat(skillData.hot.point) + parseFloat(skillData.other.point)).toFixed(2);
18 | const skillLevel = parseInt(skillPoint / 500) * 500;
19 |
20 | if (!newKasegiResult[skillLevel]) {
21 | newKasegiResult[skillLevel] = {
22 | hot: {},
23 | other: {},
24 | count: 1
25 | };
26 | } else {
27 | newKasegiResult[skillLevel].count += 1;
28 | }
29 |
30 | const addToKasegiResult = hotType => item => {
31 | const key = `${item.name} ${item.diff}-${item.part}`;
32 |
33 | if (!newKasegiResult[skillLevel][hotType][key]) {
34 | newKasegiResult[skillLevel][hotType][key] = {
35 | name: item.name,
36 | diff: item.diff,
37 | part: item.part,
38 | diffValue: item.diff_value,
39 | skills: [item.skill_value],
40 | playerSkills: [skillPoint]
41 | };
42 | } else {
43 | const { skills, playerSkills, ...rest } = newKasegiResult[skillLevel][hotType][key];
44 | newKasegiResult[skillLevel][hotType][key] = {
45 | ...rest,
46 | skills: skills.concat(item.skill_value),
47 | playerSkills: playerSkills.concat(skillPoint)
48 | };
49 | }
50 | };
51 |
52 | skillData.hot.data.forEach(addToKasegiResult("hot"));
53 | skillData.other.data.forEach(addToKasegiResult("other"));
54 |
55 | return newKasegiResult;
56 | }
57 |
58 | function extractKasegiResult(gatheredKasegiResult) {
59 | const kasegiResult = {};
60 | Object.keys(gatheredKasegiResult).forEach(skillLevel => {
61 | kasegiResult[skillLevel] = {
62 | hot: [],
63 | other: [],
64 | count: gatheredKasegiResult[skillLevel].count
65 | };
66 |
67 | const extractKasegiResultByType = hotType => {
68 | let gatheredResult = gatheredKasegiResult[skillLevel][hotType];
69 | Object.keys(gatheredResult).forEach(key => {
70 | const { skills, playerSkills, ...rest } = gatheredResult[key];
71 |
72 | kasegiResult[skillLevel][hotType].push({
73 | ...rest,
74 | averageSkill: (skills.map(parseFloat).reduce((cur, acc) => cur + acc) / skills.length).toFixed(2),
75 | count: skills.length,
76 | averagePlayerSKill: (
77 | playerSkills.map(parseFloat).reduce((cur, acc) => cur + acc) / playerSkills.length
78 | ).toFixed(2)
79 | });
80 | });
81 | };
82 |
83 | extractKasegiResultByType("hot");
84 | extractKasegiResultByType("other");
85 | });
86 |
87 | return kasegiResult;
88 | }
89 |
90 | async function kasegi({ version, type }) {
91 | const sql = `select * from skill where version=$$${version}$$ order by "playerId" asc;`;
92 |
93 | const result = await pg.query(sql);
94 | let gatheredKasegiResult = {};
95 | result.rows.forEach(userData => {
96 | try {
97 | const skillData = userData[`${type}Skill`];
98 |
99 | if (isValidSkillData(skillData)) {
100 | gatheredKasegiResult = gatherKasegiResult({
101 | skillData,
102 | kasegiResult: gatheredKasegiResult
103 | });
104 | }
105 | } catch (e) {
106 | console.error(e);
107 | }
108 | });
109 |
110 | const kasegiResult = extractKasegiResult(gatheredKasegiResult);
111 | Object.keys(kasegiResult).forEach(async skillLevel => {
112 | const result = kasegiResult[skillLevel];
113 |
114 | const listHotString = JSON.stringify(result.hot);
115 | const listOtherString = JSON.stringify(result.other);
116 | const count = result.count;
117 | const sql = `
118 | do $sql$
119 | begin
120 | update kasegi set list_hot=$$${listHotString}$$, list_other=$$${listOtherString}$$, count=$$${count}$$ where version=$$${version}$$ and type=$$${type}$$ and scope=${skillLevel};
121 | IF NOT FOUND THEN
122 | insert into kasegi values ($$${version}$$,$$${type}$$,${skillLevel},$$${listHotString}$$,$$${listOtherString}$$,${count});
123 | END IF;
124 | end
125 | $sql$
126 | `;
127 |
128 | await pg.query(sql);
129 | console.log(`Update kasegi data successfully for ${version} ${type} ${skillLevel} `);
130 | });
131 | }
132 |
133 | module.exports = {
134 | job: () => {
135 | kasegi({
136 | version: CURRENT_VERSION,
137 | type: "guitar"
138 | });
139 | kasegi({
140 | version: CURRENT_VERSION,
141 | type: "drum"
142 | });
143 | },
144 | // every day 20:00 UTC = 5:00 JST
145 | cronSchedule: "0 0 20 * * *"
146 | // cronSchedule: "0 0 * * * *" // for testing
147 | };
148 |
--------------------------------------------------------------------------------
/src/jobs/kasegi_new.js:
--------------------------------------------------------------------------------
1 | const { CURRENT_VERSION } = require("../constants");
2 | const pg = require("../modules/pg");
3 |
4 | function isValidSkillData(skillData) {
5 | return (
6 | skillData.hot &&
7 | skillData.hot.data &&
8 | skillData.hot.data.length >= 25 &&
9 | skillData.other &&
10 | skillData.other.data &&
11 | skillData.other.data.length >= 25
12 | );
13 | }
14 |
15 | function gatherKasegiResult({ skillData, kasegiResult }) {
16 | const newKasegiResult = Object.assign({}, kasegiResult);
17 | const skillPoint = (parseFloat(skillData.hot.point) + parseFloat(skillData.other.point)).toFixed(2);
18 | const skillLevel = Math.floor(skillPoint / 250) * 250;
19 |
20 | if (!newKasegiResult[skillLevel]) {
21 | newKasegiResult[skillLevel] = {
22 | hot: {},
23 | other: {},
24 | count: 1
25 | };
26 | } else {
27 | newKasegiResult[skillLevel].count += 1;
28 | }
29 |
30 | const addToKasegiResult = hotType => item => {
31 | const key = `${item.name} ${item.diff}-${item.part}`;
32 |
33 | if (!newKasegiResult[skillLevel][hotType][key]) {
34 | newKasegiResult[skillLevel][hotType][key] = {
35 | name: item.name,
36 | diff: item.diff,
37 | part: item.part,
38 | diffValue: item.diff_value,
39 | skills: [item.skill_value],
40 | playerSkills: [skillPoint]
41 | };
42 | } else {
43 | const { skills, playerSkills, ...rest } = newKasegiResult[skillLevel][hotType][key];
44 | newKasegiResult[skillLevel][hotType][key] = {
45 | ...rest,
46 | skills: skills.concat(item.skill_value),
47 | playerSkills: playerSkills.concat(skillPoint)
48 | };
49 | }
50 | };
51 |
52 | skillData.hot.data.forEach(addToKasegiResult("hot"));
53 | skillData.other.data.forEach(addToKasegiResult("other"));
54 |
55 | return newKasegiResult;
56 | }
57 |
58 | function extractKasegiResult(gatheredKasegiResult) {
59 | const kasegiResult = {};
60 | Object.keys(gatheredKasegiResult).forEach(skillLevel => {
61 | kasegiResult[skillLevel] = {
62 | hot: [],
63 | other: [],
64 | count: gatheredKasegiResult[skillLevel].count
65 | };
66 |
67 | const extractKasegiResultByType = hotType => {
68 | let gatheredResult = gatheredKasegiResult[skillLevel][hotType];
69 | Object.keys(gatheredResult).forEach(key => {
70 | const { skills, playerSkills, ...rest } = gatheredResult[key];
71 |
72 | kasegiResult[skillLevel][hotType].push({
73 | ...rest,
74 | averageSkill: (skills.map(parseFloat).reduce((cur, acc) => cur + acc) / skills.length).toFixed(2),
75 | count: skills.length,
76 | averagePlayerSKill: (
77 | playerSkills.map(parseFloat).reduce((cur, acc) => cur + acc) / playerSkills.length
78 | ).toFixed(2)
79 | });
80 | });
81 | };
82 |
83 | extractKasegiResultByType("hot");
84 | extractKasegiResultByType("other");
85 | });
86 |
87 | return kasegiResult;
88 | }
89 |
90 | async function kasegi({ version, type }) {
91 | const sql = `select * from skill where version=$$${version}$$ order by "playerId" asc;`;
92 |
93 | const result = await pg.query(sql);
94 | let gatheredKasegiResult = {};
95 | result.rows.forEach(userData => {
96 | try {
97 | const skillData = userData[`${type}Skill`];
98 |
99 | if (isValidSkillData(skillData)) {
100 | gatheredKasegiResult = gatherKasegiResult({
101 | skillData,
102 | kasegiResult: gatheredKasegiResult
103 | });
104 | }
105 | } catch (e) {
106 | console.error(e);
107 | }
108 | });
109 |
110 | const kasegiResult = extractKasegiResult(gatheredKasegiResult);
111 | Object.keys(kasegiResult).forEach(async skillLevel => {
112 | const result = kasegiResult[skillLevel];
113 |
114 | const listHotString = JSON.stringify(result.hot);
115 | const listOtherString = JSON.stringify(result.other);
116 | const count = result.count;
117 | const sql = `
118 | do $sql$
119 | begin
120 | update kasegi_new set list_hot=$$${listHotString}$$, list_other=$$${listOtherString}$$, count=$$${count}$$ where version=$$${version}$$ and type=$$${type}$$ and scope=${skillLevel};
121 | IF NOT FOUND THEN
122 | insert into kasegi_new values ($$${version}$$,$$${type}$$,${skillLevel},$$${listHotString}$$,$$${listOtherString}$$,${count});
123 | END IF;
124 | end
125 | $sql$
126 | `;
127 |
128 | await pg.query(sql);
129 | console.log(`Update kasegi_new data successfully for ${version} ${type} ${skillLevel} `);
130 | });
131 | }
132 |
133 | module.exports = {
134 | job: () => {
135 | kasegi({
136 | version: CURRENT_VERSION,
137 | type: "guitar"
138 | });
139 | kasegi({
140 | version: CURRENT_VERSION,
141 | type: "drum"
142 | });
143 | },
144 | // every day 20:00 UTC = 5:00 JST
145 | cronSchedule: "0 0 20 * * *"
146 | // cronSchedule: "0 0 * * * *" // for testing
147 | };
148 |
--------------------------------------------------------------------------------
/src/jobs/sitemap.js:
--------------------------------------------------------------------------------
1 | const convert = require("xml-js");
2 | const fs = require("fs");
3 |
4 | const { CURRENT_VERSION, ALL_VERSIONS } = require("../constants");
5 |
6 | const sitemapSource = [
7 | {
8 | path: ""
9 | },
10 | {
11 | path: "/uservoice"
12 | }
13 | ]
14 | .concat(
15 | ALL_VERSIONS.map(version => ({
16 | path: `/${version}/list`
17 | }))
18 | )
19 | .concat(
20 | Array(27)
21 | .fill(1)
22 | .map((x, i) => i * 250 + 3000)
23 | .map(scope => [
24 | {
25 | path: `/${CURRENT_VERSION}/kasegi/g/${scope}`,
26 | changefreq: "monthly"
27 | },
28 | {
29 | path: `/${CURRENT_VERSION}/kasegi/d/${scope}`,
30 | changefreq: "monthly"
31 | }
32 | ])
33 | .reduce((acc, cur) => acc.concat(cur), [])
34 | );
35 |
36 | const siteMapJs = {
37 | _declaration: {
38 | _attributes: {
39 | version: "1.0",
40 | encoding: "utf-8"
41 | }
42 | },
43 | urlset: {
44 | _attributes: {
45 | xmlns: "http://www.sitemaps.org/schemas/sitemap/0.9",
46 | "xmlns:xhtml": "http://www.w3.org/1999/xhtml"
47 | },
48 | url: sitemapSource.map(source => {
49 | let result = {
50 | loc: `http://gsv.fun/ja${source.path}`,
51 | "xhtml:link": ["ja", "en", "zh", "ko"].map(lang => ({
52 | _attributes: {
53 | rel: "alternate",
54 | hreflang: lang,
55 | href: `http://gsv.fun/${lang}${source.path}`
56 | }
57 | }))
58 | };
59 |
60 | if (source.changefreq) {
61 | result = {
62 | ...result,
63 | changefreq: source.changefreq
64 | };
65 | }
66 |
67 | return result;
68 | })
69 | }
70 | };
71 |
72 | const sitemapXML = convert.js2xml(siteMapJs, {
73 | spaces: 2,
74 | compact: true
75 | });
76 |
77 | fs.writeFileSync("sitemap.xml", sitemapXML);
78 |
--------------------------------------------------------------------------------
/src/modules/fetch.js:
--------------------------------------------------------------------------------
1 | require("es6-promise").polyfill();
2 | require("isomorphic-fetch");
3 |
4 | // XXX: the name is fetchHahaha but we can import it as fetch since it's exported by default
5 | export default function fetchHahaha(url, ...args) {
6 | if (typeof window === "undefined") {
7 | return fetch(`http://localhost:${process.env.PORT}${url}`, ...args);
8 | } else {
9 | return fetch(url, ...args);
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/modules/pg.js:
--------------------------------------------------------------------------------
1 | const { Pool } = require("pg");
2 |
3 | const pool = new Pool({
4 | connectionString: process.env.MY_DATABASE_URL,
5 | ssl: process.env.PG_SSL_ON || false
6 | });
7 |
8 | module.exports = {
9 | query: (text, params, callback) => {
10 | return pool.query(text, params, callback);
11 | }
12 | };
13 |
--------------------------------------------------------------------------------
/src/modules/query.js:
--------------------------------------------------------------------------------
1 | const URLSearchParams = require("url-search-params");
2 |
3 | function queryParser(url) {
4 | const queryString = url.split("?")[1];
5 | const query = {};
6 |
7 | if (queryString) {
8 | let searchParams = new URLSearchParams(queryString);
9 | for (let param of searchParams) {
10 | query[param[0]] = param[1];
11 | }
12 | }
13 |
14 | return query;
15 | }
16 |
17 | module.exports = queryParser;
18 |
--------------------------------------------------------------------------------
/src/public/image/._2-1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ssdh233/gitadora-skill-viewer/e388f34ec10174655e1f9bbcdf887d75f6f31e84/src/public/image/._2-1.jpg
--------------------------------------------------------------------------------
/src/public/image/._2-2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ssdh233/gitadora-skill-viewer/e388f34ec10174655e1f9bbcdf887d75f6f31e84/src/public/image/._2-2.jpg
--------------------------------------------------------------------------------
/src/public/image/._addFriend.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ssdh233/gitadora-skill-viewer/e388f34ec10174655e1f9bbcdf887d75f6f31e84/src/public/image/._addFriend.jpg
--------------------------------------------------------------------------------
/src/public/image/._sharedSongEdit.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ssdh233/gitadora-skill-viewer/e388f34ec10174655e1f9bbcdf887d75f6f31e84/src/public/image/._sharedSongEdit.jpg
--------------------------------------------------------------------------------
/src/public/image/1-1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ssdh233/gitadora-skill-viewer/e388f34ec10174655e1f9bbcdf887d75f6f31e84/src/public/image/1-1.jpg
--------------------------------------------------------------------------------
/src/public/image/1-2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ssdh233/gitadora-skill-viewer/e388f34ec10174655e1f9bbcdf887d75f6f31e84/src/public/image/1-2.jpg
--------------------------------------------------------------------------------
/src/public/image/1-3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ssdh233/gitadora-skill-viewer/e388f34ec10174655e1f9bbcdf887d75f6f31e84/src/public/image/1-3.jpg
--------------------------------------------------------------------------------
/src/public/image/2-1.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ssdh233/gitadora-skill-viewer/e388f34ec10174655e1f9bbcdf887d75f6f31e84/src/public/image/2-1.jpeg
--------------------------------------------------------------------------------
/src/public/image/2-1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ssdh233/gitadora-skill-viewer/e388f34ec10174655e1f9bbcdf887d75f6f31e84/src/public/image/2-1.jpg
--------------------------------------------------------------------------------
/src/public/image/2-2.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ssdh233/gitadora-skill-viewer/e388f34ec10174655e1f9bbcdf887d75f6f31e84/src/public/image/2-2.jpeg
--------------------------------------------------------------------------------
/src/public/image/2-2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ssdh233/gitadora-skill-viewer/e388f34ec10174655e1f9bbcdf887d75f6f31e84/src/public/image/2-2.jpg
--------------------------------------------------------------------------------
/src/public/image/2-3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ssdh233/gitadora-skill-viewer/e388f34ec10174655e1f9bbcdf887d75f6f31e84/src/public/image/2-3.jpg
--------------------------------------------------------------------------------
/src/public/image/addFriend.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ssdh233/gitadora-skill-viewer/e388f34ec10174655e1f9bbcdf887d75f6f31e84/src/public/image/addFriend.jpg
--------------------------------------------------------------------------------
/src/public/image/sharedSongEdit.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ssdh233/gitadora-skill-viewer/e388f34ec10174655e1f9bbcdf887d75f6f31e84/src/public/image/sharedSongEdit.jpg
--------------------------------------------------------------------------------
/src/public/js/google_analytics.js:
--------------------------------------------------------------------------------
1 | (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
2 | (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
3 | m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
4 | })(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
5 |
6 | ga('create', 'UA-89581468-2', 'auto');
7 | ga('send', 'pageview');
--------------------------------------------------------------------------------
/src/react/App.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import styled, { ThemeProvider } from "styled-components";
3 | import { Route, Switch, withRouter } from "react-router-dom";
4 |
5 | import IndexPage from "./IndexPage";
6 | import AppHeader from "./AppHeader.jsx";
7 | import UserVoicePage from "./UserVoicePage.jsx";
8 | import KasegiPage from "./KasegiPage";
9 | import KasegiIndexPage from "./KasegiPage/KasegiIndexPage.jsx";
10 | import KasegiNewPage from "./KasegiNewPage";
11 | import ListPage from "./ListPage";
12 | import SkillPageContainer, { SavedSkillPageContainer } from "./SkillPage";
13 | import SharedSongsPage from "./SharedSongsPage";
14 | import ErrorListPage from "./ErrorListPage";
15 | import theme from "../theme.js";
16 | import { Helmet } from "react-helmet";
17 | import { ThemeProvider as MuiThemeProvider } from "@material-ui/styles";
18 | import { createTheme } from "@material-ui/core";
19 |
20 | function App({ initialThemeKey = "default" }) {
21 | const [themeKey, setThemeKey] = useState(initialThemeKey);
22 |
23 | useEffect(() => {
24 | const jssStyles = document.getElementById("jss-server-side");
25 | if (jssStyles && jssStyles.parentNode) {
26 | jssStyles.parentNode.removeChild(jssStyles);
27 | }
28 | }, []);
29 |
30 | return (
31 |
32 |
39 |
40 |
41 |
42 |
67 |
68 | } />
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 | } />
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 | );
87 | }
88 |
89 | const Background = styled.div`
90 | min-height: 100vh;
91 | padding: 8px;
92 | box-sizing: border-box;
93 | `;
94 |
95 | const MainContainer = styled.div`
96 | font-family: verdana;
97 | font-size: 16px;
98 | max-width: 1200px;
99 | margin: auto;
100 |
101 | @media (max-width: 742px) {
102 | font-size: 14px;
103 | }
104 | `;
105 |
106 | export default withRouter(App);
107 |
--------------------------------------------------------------------------------
/src/react/ErrorListPage/ErrorListPage.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 |
4 | function ErrorListPage({ errors }) {
5 | if (!errors) return null;
6 | return errors.map((error, index) => {
7 | return (
8 |
9 | {error.version}
10 | {error.error}
11 | {error.date}
12 | {error.userAgent}
13 |
14 | );
15 | });
16 | }
17 |
18 | const ErrorSection = styled.div`
19 | margin-bottom: 20px;
20 | `;
21 |
22 | export default ErrorListPage;
23 |
--------------------------------------------------------------------------------
/src/react/ErrorListPage/ErrorListPageContainer.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useQuery } from "@apollo/react-hooks";
3 | import gql from "graphql-tag";
4 |
5 | import ErrorListPage from "./ErrorListPage.jsx";
6 |
7 | const FETCH_ERRORS = gql`
8 | {
9 | errors {
10 | error
11 | userAgent
12 | version
13 | date
14 | }
15 | }
16 | `;
17 |
18 | function ErrorListPageContainer() {
19 | const { data } = useQuery(FETCH_ERRORS);
20 | return ;
21 | }
22 |
23 | export default ErrorListPageContainer;
24 |
--------------------------------------------------------------------------------
/src/react/ErrorListPage/index.js:
--------------------------------------------------------------------------------
1 | import ErrorListPageContainer from "./ErrorListPageContainer.jsx";
2 |
3 | export default ErrorListPageContainer;
4 |
--------------------------------------------------------------------------------
/src/react/IndexPage/BookmarkletScript.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import styled from "styled-components";
3 | import Select from "@material-ui/core/Select";
4 | import MenuItem from "@material-ui/core/MenuItem";
5 | import { FormattedMessage } from "react-intl";
6 | import FileCopyIcon from "@material-ui/icons/FileCopy";
7 | import IconButton from "@material-ui/core/IconButton";
8 |
9 | import { CURRENT_VERSION, CURRENT_VERSION_2, ON_SERVICE_VERSIONS, VERSION_NAME } from "../../constants";
10 | import { Snackbar } from "@material-ui/core";
11 |
12 | function BookmarkletScript() {
13 | const [select, setSelect] = useState(ON_SERVICE_VERSIONS[2]);
14 | const [snackbarOpen, setSnackbarOpen] = useState(false);
15 |
16 | const latestScript = `javascript:void(!function(d){var s=d.createElement('script');s.type='text/javascript';s.src='//gitadora-skill-viewer.herokuapp.com/js/uploaddata_latest.js';d.head.appendChild(s);}(document));`;
17 | const latestScript2 = `javascript:void(!function(d){var s=d.createElement('script');s.type='text/javascript';s.src='//gitadora-skill-viewer.herokuapp.com/js/uploaddata_galaxywave_delta.js';d.head.appendChild(s);}(document));`;
18 | const versionScript = `javascript:void(!function(d){var s=d.createElement('script');s.type='text/javascript';s.src='//gitadora-skill-viewer.herokuapp.com/js/uploaddata_${select}.js';d.head.appendChild(s);}(document));`;
19 |
20 | useEffect(() => {
21 | if (snackbarOpen) {
22 | setTimeout(() => {
23 | setSnackbarOpen(false);
24 | }, 3000);
25 | }
26 | }, [snackbarOpen]);
27 |
28 | return (
29 | <>
30 |
31 |
32 | {" " + VERSION_NAME[CURRENT_VERSION] + " "}
33 |
34 |
35 |
36 |
37 |
38 |
39 | {latestScript}
40 | {
42 | navigator.clipboard.writeText(latestScript);
43 | setSnackbarOpen(true);
44 | }}
45 | >
46 |
47 |
48 |
49 |
50 |
51 | {" " + VERSION_NAME[CURRENT_VERSION_2] + " "}
52 |
53 |
54 |
55 | {latestScript2}
56 | {
58 | navigator.clipboard.writeText(latestScript2);
59 | setSnackbarOpen(true);
60 | }}
61 | >
62 |
63 |
64 |
65 |
66 |
67 |
68 | {
73 | setSelect(event.target.value);
74 | }}
75 | >
76 | {ON_SERVICE_VERSIONS.slice(2).map(version => (
77 |
80 | ))}
81 |
82 |
83 |
84 |
85 | {versionScript}
86 | {
88 | navigator.clipboard.writeText(versionScript);
89 | setSnackbarOpen(true);
90 | }}
91 | >
92 |
93 |
94 |
95 |
96 |
104 |
105 |
106 | }
107 | />
108 | >
109 | );
110 | }
111 |
112 | const ScriptDiv = styled.div`
113 | background: ${({ theme }) => theme.index.scriptBg};
114 | border-radius: 6px;
115 | font-size: 80%;
116 | word-break: break-all;
117 | margin: 16px;
118 | display: flex;
119 |
120 | > span {
121 | padding: 16px 0 16px 16px;
122 | }
123 | `;
124 |
125 | const MuiIconButton = styled(IconButton)`
126 | &&& {
127 | align-self: center;
128 | cursor: pointer;
129 | color: ${({ theme }) => theme.header.button};
130 | }
131 | `;
132 |
133 | const MuiSelect = styled(Select)`
134 | &&& {
135 | color: ${({ theme }) => theme.main};
136 | font-size: 16px;
137 |
138 | @media (max-width: 742px) {
139 | font-size: 14px;
140 | }
141 |
142 | > svg {
143 | color: ${({ theme }) => theme.main};
144 | }
145 |
146 | ::before {
147 | border-bottom: 1px solid ${({ theme }) => theme.main};
148 | }
149 |
150 | ::after {
151 | border-bottom: 2px solid ${({ theme }) => theme.main};
152 | }
153 | }
154 | `;
155 |
156 | const MuiSnackbar = styled(Snackbar)`
157 | & .MuiPaper-root {
158 | background: ${({ theme }) => theme.index.snackBarBg};
159 | min-width: unset;
160 | }
161 | `;
162 |
163 | const Desc = styled.span`
164 | font-size: 14px;
165 |
166 | @media (max-width: 742px) {
167 | font-size: 12px;
168 | }
169 | `;
170 |
171 | export default BookmarkletScript;
172 |
--------------------------------------------------------------------------------
/src/react/IndexPage/HowToUseSection.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { FormattedMessage, FormattedHTMLMessage } from "react-intl";
3 | import LazyLoad from "react-lazyload";
4 | import HelpOutlineIcon from "@material-ui/icons/HelpOutline";
5 |
6 | import BookmarkletScript from "./BookmarkletScript.jsx";
7 | import styled, { withTheme } from "styled-components";
8 | import { Alert } from "@material-ui/lab";
9 | import { Button, Dialog, DialogActions, DialogContent, DialogContentText } from "@material-ui/core";
10 |
11 | function HowToUse(props) {
12 | const { theme } = props;
13 |
14 | const [showDialog, setShowDialog] = useState(false);
15 |
16 | return (
17 | <>
18 | {}
19 |
20 |
24 |
25 |
26 |
27 |
28 |
29 |
35 |
36 |
37 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 | (
72 |
73 | {content}
74 |
75 | ),
76 | code: content => {content}
,
77 | steps: () => (
78 |
79 | -
80 |
84 |
85 | -
86 |
90 |
91 | -
92 | (
97 | setShowDialog(true)}
104 | />
105 | ),
106 | code: content =>
{content}
107 | }}
108 | />
109 |
110 | -
111 |
115 |
116 |
117 | )
118 | }}
119 | />
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 | setShowDialog(false)}>
133 |
134 |
135 |
136 |
137 |
138 |
139 |
142 |
143 |
144 | >
145 | );
146 | }
147 |
148 | const MuiAlert = styled(Alert)`
149 | &&& {
150 | font-size: 14px;
151 | opacity: ${({ theme }) => theme.index.alertOpacity};
152 | margin: 16px;
153 |
154 | @media (max-width: 742px) {
155 | font-size: 12px;
156 | margin: 12px;
157 | }
158 | }
159 | `;
160 |
161 | const ImageContainer = styled.div`
162 | position: relative;
163 | color: ${({ theme }) => theme.index.imageDesc};
164 | whitespace: nowrap;
165 | `;
166 |
167 | const Image = styled.img`
168 | opacity: ${({ theme }) => theme.index.imageOpacity};
169 | `;
170 |
171 | const ImageDesc = styled.div`
172 | position: absolute;
173 | color: ${({ theme }) => theme.index.imageDesc};
174 | background: ${({ theme }) => theme.index.imageDescBg};
175 | `;
176 |
177 | const MuiDialog = styled(Dialog)`
178 | & .MuiPaper-root {
179 | background-color: ${({ theme }) => theme.mainBg};
180 | }
181 |
182 | & .MuiTypography-root {
183 | color: ${({ theme }) => theme.main};
184 | }
185 |
186 | & .MuiButtonBase-root {
187 | color: ${({ theme }) => theme.link};
188 | }
189 | `;
190 |
191 | export default withTheme(HowToUse);
192 |
--------------------------------------------------------------------------------
/src/react/IndexPage/IndexPage.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { Link } from "react-router-dom";
3 | import { Helmet } from "react-helmet";
4 | import { FormattedMessage, FormattedHTMLMessage, injectIntl } from "react-intl";
5 | import Snackbar from "@material-ui/core/Snackbar";
6 | import Button from "@material-ui/core/Button";
7 | import styled, { withTheme } from "styled-components";
8 |
9 | import SlideToggle from "../SlideToggle.jsx";
10 | import { CURRENT_VERSION } from "../../constants";
11 | import HowToUseSection from "./HowToUseSection.jsx";
12 | import OtherSection from "./OtherSection.jsx";
13 |
14 | function IndexPage(props) {
15 | const [snackbarOpen, setSnackbarOpen] = useState(false);
16 | const [gsvId, setGsvId] = useState("");
17 | const [gsvName, setGsvName] = useState("");
18 |
19 | useEffect(() => {
20 | const gsvId = localStorage.getItem("gsvId");
21 | const gsvName = localStorage.getItem("gsvName");
22 | if (gsvId) {
23 | setSnackbarOpen(true);
24 | setGsvId(gsvId);
25 | setGsvName(gsvName);
26 | }
27 | }, []);
28 |
29 | const {
30 | intl: { formatMessage },
31 | match: {
32 | params: { locale }
33 | }
34 | } = props;
35 |
36 | return (
37 |
38 |
39 |
40 | {formatMessage({ id: "title" })}
41 |
49 |
50 |
58 |
59 |
62 |
63 |
66 |
67 | }
68 | ContentProps={{
69 | "aria-describedby": "message-id"
70 | }}
71 | message={
72 |
73 |
74 |
75 | }
76 | />
77 | }>
78 |
79 | {
80 |
84 | }
85 |
86 | {}
87 |
88 | -
89 |
90 |
91 | -
92 |
96 |
97 |
98 |
99 | }>
100 |
101 |
102 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 | } titleId="otherSlideToggle">
115 |
116 |
117 |
118 | );
119 | }
120 |
121 | const IndexPageContainer = styled.div`
122 | width: 100%;
123 | `;
124 |
125 | const MuiSnackbar = styled(Snackbar)`
126 | & .MuiPaper-root {
127 | background: ${({ theme }) => theme.index.snackBarBg};
128 | min-width: unset;
129 | }
130 | `;
131 |
132 | export default withTheme(injectIntl(IndexPage));
133 |
--------------------------------------------------------------------------------
/src/react/IndexPage/OtherSection.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { FormattedMessage, injectIntl } from "react-intl";
3 | import AttachMoney from "@material-ui/icons/AttachMoney";
4 | import Popover from "@material-ui/core/Popover";
5 | import List from "@material-ui/core/List";
6 | import { Link } from "react-router-dom";
7 | import ListItem from "@material-ui/core/ListItem";
8 | import ListItemText from "@material-ui/core/ListItemText";
9 | import FormatListBulleted from "@material-ui/icons/FormatListBulleted";
10 |
11 | import { MuiButton, MuiMenuItem, MuiMenuList } from "../AppHeader.jsx";
12 | import styled, { withTheme } from "styled-components";
13 | import { ListSubheader } from "@material-ui/core";
14 | import { withRouter } from "react-router-dom/cjs/react-router-dom.min.js";
15 | import { ALL_VERSIONS, CURRENT_VERSION, VERSION_NAME } from "../../constants.js";
16 |
17 | function OtherSection(props) {
18 | const {
19 | theme,
20 | intl: { formatMessage },
21 | match: {
22 | params: { locale }
23 | }
24 | } = props;
25 |
26 | const [kasegiAnchorEl, setKasegiAnchorEl] = useState();
27 | const [listAnchorEl, setListAnchorEl] = useState();
28 |
29 | return (
30 | <>
31 |
32 | {"★"}
33 |
34 |
35 | setKasegiAnchorEl(event.currentTarget)}
38 | aria-haspopup={true}
39 | >
40 |
41 |
42 |
43 |
44 |
45 | setKasegiAnchorEl(null)}
49 | onClick={() => setKasegiAnchorEl(null)}
50 | PaperProps={{
51 | style: {
52 | background: theme.header.popoverBg
53 | }
54 | }}
55 | >
56 |
57 |
66 | Drum
67 | Guitar
68 |
69 |
77 | {[...Array(13).keys()].reverse().map(key => {
78 | const skill = 3000 + key * 500;
79 | return (
80 | <>
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 | >
92 | );
93 | })}
94 |
95 |
96 |
97 | {
101 | setListAnchorEl(event.currentTarget);
102 | }}
103 | >
104 |
105 |
106 |
107 |
108 |
109 | setListAnchorEl(null)}
114 | onClick={() => setListAnchorEl(null)}
115 | PaperProps={{
116 | style: {
117 | background: props.theme.header.popoverBg
118 | }
119 | }}
120 | >
121 |
122 | {ALL_VERSIONS.slice(1).map(version => (
123 |
124 | {VERSION_NAME[version].replace(":EVOLVE", "")}
125 |
126 | ))}
127 |
128 |
129 |
130 |
131 | {"★" +
132 | formatMessage({
133 | id: "other.code.title"
134 | }) +
135 | ":"}
136 |
137 | Github
138 |
139 |
140 |
141 | {"★"}
142 |
143 | {":"}
144 | User Voice
145 |
146 |
147 |
148 |
149 |
150 | {"★"}
151 |
152 |
153 | Chrome, Safari
154 | >
155 | );
156 | }
157 |
158 | const MuiListSubHeader = styled(ListSubheader)`
159 | &&& {
160 | color: ${({ theme }) => theme.header.popoverHeader};
161 | }
162 | `;
163 |
164 | export default withTheme(withRouter(injectIntl(OtherSection)));
165 |
--------------------------------------------------------------------------------
/src/react/IndexPage/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./IndexPage.jsx";
2 |
--------------------------------------------------------------------------------
/src/react/KasegiNewPage/KasegiTable.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactTable from "react-table";
3 | import { FormattedMessage } from "react-intl";
4 | import { Helmet } from "react-helmet";
5 | import styled, { withTheme } from "styled-components";
6 |
7 | import useMediaQuery from "@material-ui/core/useMediaQuery";
8 |
9 | function KasegiTable(props) {
10 | const isPC = useMediaQuery("(min-width:742px)");
11 | const getTrProps = (state, rowInfo) => {
12 | const diff = (rowInfo && rowInfo.original && rowInfo.original.diff) || "";
13 | return {
14 | style: {
15 | backgroundColor:
16 | {
17 | BAS: "#C7E7FF",
18 | ADV: "#FFFFC7",
19 | EXT: "#FFC7C7",
20 | MAS: "#D8BFF8"
21 | }[diff] + props.theme.kasegi.tableBgHexOpacity
22 | }
23 | };
24 | };
25 |
26 | const getTdProps = (state, rowInfo, column) => {
27 | let style = {
28 | display: "flex",
29 | flexDirection: "column",
30 | justifyContent: "center"
31 | };
32 | let className;
33 | if (rowInfo && rowInfo.row.compare && column.id === "compare") {
34 | if (rowInfo.row.compare.includes("↑")) {
35 | className = "higherThanAverage";
36 | }
37 | if (rowInfo.row.compare.includes("↓")) {
38 | className = "lowerThanAverage";
39 | }
40 | }
41 |
42 | return { className, style };
43 | };
44 |
45 | const getTheadProps = () => ({
46 | style: { height: 30 }
47 | });
48 |
49 | let columns = [
50 | {
51 | Header: "No.",
52 | accessor: "index",
53 | maxWidth: 40
54 | },
55 | {
56 | Header: () => ,
57 | accessor: "name",
58 | minWidth: isPC ? 180 : 150,
59 | style: {
60 | whiteSpace: "unset"
61 | }
62 | },
63 | {
64 | Header: () => ,
65 | accessor: "displayedDiff",
66 | maxWidth: isPC ? 100 : 53,
67 | style: {
68 | whiteSpace: "unset",
69 | textAlign: "center"
70 | }
71 | },
72 | {
73 | Header: () => ,
74 | accessor: "displayedAverageSkill",
75 | maxWidth: isPC ? 140 : 70,
76 | style: {
77 | whiteSpace: "unset",
78 | textAlign: "center"
79 | },
80 | sortMethod: (a, b) => {
81 | let skillA = Number(a.split("(")[0]);
82 | let skillB = Number(b.split("(")[0]);
83 | return skillA - skillB;
84 | }
85 | }
86 | ];
87 |
88 | if (!props.hasComparedSkill || props.mediaQuery === "pc") {
89 | columns = [
90 | ...columns,
91 | {
92 | Header: () => ,
93 | accessor: "count",
94 | maxWidth: isPC ? 70 : 60,
95 | style: {
96 | textAlign: "center"
97 | },
98 | Cell: data => <>{((data.value / props.count) * 100).toFixed(2)}%>
99 | }
100 | ].filter(x => x);
101 | }
102 |
103 | if (props.hasComparedSkill) {
104 | columns = [
105 | ...columns,
106 | {
107 | Header: () => ,
108 | accessor: "compare",
109 | maxWidth: isPC ? 60 : 50,
110 | textAlign: "center",
111 | sortMethod: (a, b, desc) => {
112 | if (!b) return !desc ? -1 : 1;
113 | if (!a) return !desc ? 1 : -1;
114 | let aNum = Number(a.slice(0, a.length - 1));
115 | if (a[a.length - 1] === "↓") aNum = -aNum;
116 | let bNum = Number(b.slice(0, b.length - 1));
117 | if (b[b.length - 1] === "↓") bNum = -bNum;
118 | return aNum - bNum;
119 | }
120 | }
121 | ].filter(x => x);
122 | }
123 |
124 | return (
125 | <>
126 |
127 |
128 |
129 |
130 |
144 |
145 | >
146 | );
147 | }
148 |
149 | const KasegiTableRoot = styled.div`
150 | font-size: 14px;
151 | color: ${({ theme }) => theme.kasegi.table};
152 | background-color: ${({ theme }) => theme.kasegi.tableBg};
153 |
154 | @media (max-width: 742px) {
155 | font-size: 12px;
156 | }
157 | `;
158 |
159 | const stringStyles = `
160 | .higherThanAverage {
161 | color: red
162 | }
163 |
164 | .lowerThanAverage {
165 | color: green
166 | }
167 | `;
168 |
169 | export default withTheme(KasegiTable);
170 |
--------------------------------------------------------------------------------
/src/react/KasegiNewPage/component.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Link } from "react-router-dom";
3 | import { Helmet } from "react-helmet";
4 | import { injectIntl, FormattedMessage } from "react-intl";
5 |
6 | import KasegiTable from "./KasegiTable.jsx";
7 | import { CURRENT_VERSION } from "../../constants.js";
8 |
9 | class KasegiNewPage extends React.Component {
10 | render() {
11 | const {
12 | kasegiData,
13 | comparedSkillData,
14 | intl: { formatMessage },
15 | match: {
16 | params: { locale, version, type, scope },
17 | query: { c: comparedSkillId }
18 | }
19 | } = this.props;
20 |
21 | const typeTitle = type === "d" ? "Drummania" : "Guitarfreaks";
22 | const typeTitleShort = type === "d" ? "DRUM" : "GUITAR";
23 | const scopeTitle = `${scope} ~ ${parseInt(scope) + 250}`;
24 |
25 | const title = `${typeTitle}${formatMessage({
26 | id: "kasegi.title"
27 | })} ${scopeTitle}`;
28 | return (
29 |
30 |
31 |
32 |
45 | {`${title} | Gitadora Skill Viewer`}
46 | {version !== CURRENT_VERSION && }
47 |
48 |
49 |
{title}
50 | {version !== CURRENT_VERSION && (
51 |
59 | ⚠️古いバージョンの情報です。最新の稼ぎ曲の情報は
60 | こちら
61 |
62 | )}
63 | {comparedSkillData && (
64 |
65 | {/* TODO add link after find a way to inject query to prop */}
66 |
71 |
77 |
78 | )
79 | }}
80 | />
81 |
82 | )}
83 | {kasegiData && kasegiData.hot && (
84 |
85 |
{`${typeTitleShort} HOT`}
86 |
91 |
92 | )}
93 | {kasegiData && kasegiData.other && (
94 |
95 |
{`${typeTitleShort} OTHER`}
96 |
101 |
102 | )}
103 |
104 |
105 | );
106 | }
107 | }
108 |
109 | const styles = {
110 | kasegiNewPage: {
111 | maxWidth: 800
112 | },
113 | title: {
114 | fontSize: 19
115 | },
116 | subtitle: {
117 | fontSize: 16
118 | },
119 | notLastDiv: {
120 | marginBottom: 20
121 | },
122 | caption: {
123 | fontSize: 14,
124 | textAlign: "center",
125 | marginBottom: 5
126 | }
127 | };
128 |
129 | export default injectIntl(KasegiNewPage);
130 |
--------------------------------------------------------------------------------
/src/react/KasegiNewPage/container.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import gql from "graphql-tag";
3 | import { useQuery } from "@apollo/react-hooks";
4 | import LinearProgress from "@material-ui/core/LinearProgress";
5 |
6 | import queryParser from "../../modules/query";
7 | import KasegiPage from "./component.jsx";
8 |
9 | // TODO move away fragments
10 | // TODO find way to fetch only guitar/drum skill (write 2 queries?)
11 | const GET_KASEGI = gql`
12 | fragment SkillRecord on SkillRecord {
13 | name
14 | part
15 | diff
16 | skill_value
17 | achive_value
18 | diff_value
19 | }
20 |
21 | fragment HalfSkillTable on HalfSkillTable {
22 | point
23 | data {
24 | ...SkillRecord
25 | }
26 | }
27 |
28 | fragment KasegiRecord on KasegiRecord {
29 | name
30 | diff
31 | part
32 | diffValue
33 | averageSkill
34 | count
35 | averagePlayerSKill
36 | }
37 |
38 | query KasegiNew($version: Version, $type: GameType, $scope: Int, $playerId: Int) {
39 | kasegiNew(version: $version, type: $type, scope: $scope) {
40 | version
41 | type
42 | scope
43 | count
44 | hot {
45 | ...KasegiRecord
46 | }
47 | other {
48 | ...KasegiRecord
49 | }
50 | }
51 | user(playerId: $playerId, version: $version) {
52 | playerId
53 | playerName
54 | drumSkill {
55 | hot {
56 | ...HalfSkillTable
57 | }
58 | other {
59 | ...HalfSkillTable
60 | }
61 | }
62 | guitarSkill {
63 | hot {
64 | ...HalfSkillTable
65 | }
66 | other {
67 | ...HalfSkillTable
68 | }
69 | }
70 | }
71 | }
72 | `;
73 |
74 | export default function KasegiPageContainer(props) {
75 | const { version, type, scope } = props.match.params;
76 | const query = queryParser(props.location.search);
77 |
78 | const { data, loading, error } = useQuery(GET_KASEGI, {
79 | variables: {
80 | version,
81 | type,
82 | scope: parseInt(scope),
83 | playerId: parseInt(query.c)
84 | }
85 | });
86 |
87 | if (loading) return ;
88 | if (error) return ERROR: {error.toString()}
;
89 |
90 | return (
91 |
99 | );
100 | }
101 |
102 | // TODO rethink about this function
103 | function getKasegiData(kasegiData, kasegiComparedSkill, type) {
104 | const sortByCountAndSkill = (a, b) => {
105 | if (a.count !== b.count) {
106 | return b.count - a.count;
107 | } else if (b.averageSkill - a.averageSkill) {
108 | return b.averageSkill - a.averageSkill;
109 | }
110 | };
111 |
112 | const processData = skillData => (data, index) => {
113 | const { name, diff, part, diffValue, averageSkill } = data;
114 |
115 | let displayedDiff = `${diffValue.toFixed(2)} ${diff}`;
116 | if (part !== "D") {
117 | displayedDiff = `${displayedDiff}-${part}`;
118 | }
119 |
120 | const averageAchieve = ((averageSkill / (diffValue * 20)) * 100).toFixed(2) + "%";
121 | const displayedAverageSkill = `${averageSkill.toFixed(2)} (${averageAchieve})`;
122 |
123 | let compare = null;
124 | if (skillData) {
125 | let comparedData = skillData.find(
126 | skillDataItem => skillDataItem.name === name && skillDataItem.diff === diff && skillDataItem.part === part
127 | );
128 | if (comparedData) {
129 | compare = comparedData.skill_value - averageSkill;
130 | if (compare > 0) {
131 | compare = `${compare.toFixed(2)}↑`;
132 | } else if (compare < 0) {
133 | compare = `${compare.toFixed(2).substring(1)}↓`;
134 | } else {
135 | compare = `0.00`;
136 | }
137 | }
138 | }
139 |
140 | return {
141 | index: index + 1,
142 | diff,
143 | displayedDiff,
144 | displayedAverageSkill,
145 | compare,
146 | ...data
147 | };
148 | };
149 |
150 | if (!kasegiData) {
151 | return null;
152 | }
153 |
154 | const { hot: kasegiHot, other: kasegiOther, ...rest } = kasegiData;
155 | const { hot: skillHot, other: skillOther } =
156 | (kasegiComparedSkill && kasegiComparedSkill[type === "d" ? "drumSkill" : "guitarSkill"]) || {};
157 |
158 | return {
159 | ...rest,
160 | hot: kasegiHot && kasegiHot.sort(sortByCountAndSkill).map(processData(skillHot && skillHot.data)),
161 | other: kasegiOther && kasegiOther.sort(sortByCountAndSkill).map(processData(skillOther && skillOther.data))
162 | };
163 | }
164 |
--------------------------------------------------------------------------------
/src/react/KasegiNewPage/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./container.jsx";
2 |
--------------------------------------------------------------------------------
/src/react/KasegiPage/KasegiIndexPage.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Link } from "react-router-dom";
3 | import List from "@material-ui/core/List";
4 | import ListItem from "@material-ui/core/ListItem";
5 | import ListItemText from "@material-ui/core/ListItemText";
6 | import { FormattedMessage } from "react-intl";
7 |
8 | import { CURRENT_VERSION } from "../../constants";
9 | import { ListSubheader } from "@material-ui/core";
10 | import styled from "styled-components";
11 |
12 | function KasegiIndexPage(props) {
13 | const {
14 | match: {
15 | params: { locale }
16 | }
17 | } = props;
18 |
19 | return (
20 | <>
21 |
22 |
23 |
24 |
25 |
34 | Drum
35 | Guitar
36 |
37 |
45 | {[...Array(13).keys()].reverse().map(key => {
46 | const skill = 3000 + key * 500;
47 | return (
48 | <>
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 | >
60 | );
61 | })}
62 |
63 |
64 | >
65 | );
66 | }
67 |
68 | export const MuiListSubHeader = styled(ListSubheader)`
69 | &&& {
70 | color: ${({ theme }) => theme.header.popoverHeader};
71 | }
72 | `;
73 |
74 | export default KasegiIndexPage;
75 |
--------------------------------------------------------------------------------
/src/react/KasegiPage/KasegiPage.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Link } from "react-router-dom";
3 | import { Helmet } from "react-helmet";
4 | import { injectIntl, FormattedMessage } from "react-intl";
5 |
6 | import KasegiTable from "./KasegiTable.jsx";
7 | import { CURRENT_VERSION } from "../../constants.js";
8 |
9 | class KasegiPage extends React.Component {
10 | render() {
11 | const {
12 | kasegiData,
13 | comparedSkillData,
14 | intl: { formatMessage },
15 | match: {
16 | params: { locale, version, type, scope },
17 | query: { c: comparedSkillId }
18 | }
19 | } = this.props;
20 |
21 | const typeTitle = type === "d" ? "Drummania" : "Guitarfreaks";
22 | const typeTitleShort = type === "d" ? "DRUM" : "GUITAR";
23 | const scopeTitle = `${scope} ~ ${parseInt(scope) + 500}`;
24 | const scopeNameColor = formatMessage({
25 | id: `kasegi.scope.${scope}`
26 | });
27 |
28 | const title = `${typeTitle}${formatMessage({
29 | id: "kasegi.title"
30 | })} ${scopeTitle} ${scopeNameColor}`;
31 | return (
32 |
33 |
34 |
35 |
48 | {`${title} | Gitadora Skill Viewer`}
49 |
50 |
51 |
52 |
{title}
53 | {version !== CURRENT_VERSION && (
54 |
62 | ⚠️古いバージョンの情報です。最新の稼ぎ曲の情報は
63 | こちら
64 |
65 | )}
66 | {comparedSkillData && (
67 |
68 | {/* TODO add link after find a way to inject query to prop */}
69 |
74 |
80 |
81 | )
82 | }}
83 | />
84 |
85 | )}
86 | {kasegiData && kasegiData.hot && (
87 |
88 |
{`${typeTitleShort} HOT`}
89 |
94 |
95 | )}
96 | {kasegiData && kasegiData.other && (
97 |
98 |
{`${typeTitleShort} OTHER`}
99 |
104 |
105 | )}
106 |
107 |
108 | );
109 | }
110 | }
111 |
112 | const styles = {
113 | kasegiPage: {
114 | maxWidth: 800
115 | },
116 | title: {
117 | fontSize: 19
118 | },
119 | subtitle: {
120 | fontSize: 16
121 | },
122 | notLastDiv: {
123 | marginBottom: 20
124 | },
125 | caption: {
126 | fontSize: 14,
127 | textAlign: "center",
128 | marginBottom: 5
129 | }
130 | };
131 |
132 | export default injectIntl(KasegiPage);
133 |
--------------------------------------------------------------------------------
/src/react/KasegiPage/KasegiPageContainer.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import gql from "graphql-tag";
3 | import { useQuery } from "@apollo/react-hooks";
4 | import LinearProgress from "@material-ui/core/LinearProgress";
5 |
6 | import queryParser from "../../modules/query";
7 | import KasegiPage from "./KasegiPage.jsx";
8 |
9 | // TODO move away fragments
10 | // TODO find way to fetch only guitar/drum skill (write 2 queries?)
11 | const GET_KASEGI = gql`
12 | fragment SkillRecord on SkillRecord {
13 | name
14 | part
15 | diff
16 | skill_value
17 | achive_value
18 | diff_value
19 | }
20 |
21 | fragment HalfSkillTable on HalfSkillTable {
22 | point
23 | data {
24 | ...SkillRecord
25 | }
26 | }
27 |
28 | fragment KasegiRecord on KasegiRecord {
29 | name
30 | diff
31 | part
32 | diffValue
33 | averageSkill
34 | count
35 | averagePlayerSKill
36 | }
37 |
38 | query Kasegi($version: Version, $type: GameType, $scope: Int, $playerId: Int) {
39 | kasegi(version: $version, type: $type, scope: $scope) {
40 | version
41 | type
42 | scope
43 | count
44 | hot {
45 | ...KasegiRecord
46 | }
47 | other {
48 | ...KasegiRecord
49 | }
50 | }
51 | user(playerId: $playerId, version: $version) {
52 | playerId
53 | playerName
54 | drumSkill {
55 | hot {
56 | ...HalfSkillTable
57 | }
58 | other {
59 | ...HalfSkillTable
60 | }
61 | }
62 | guitarSkill {
63 | hot {
64 | ...HalfSkillTable
65 | }
66 | other {
67 | ...HalfSkillTable
68 | }
69 | }
70 | }
71 | }
72 | `;
73 |
74 | export default function KasegiPageContainer(props) {
75 | const { version, type, scope } = props.match.params;
76 | const query = queryParser(props.location.search);
77 |
78 | const { data, loading, error } = useQuery(GET_KASEGI, {
79 | variables: {
80 | version,
81 | type,
82 | scope: parseInt(scope),
83 | playerId: parseInt(query.c)
84 | }
85 | });
86 |
87 | if (loading) return ;
88 | if (error) return ERROR: {error.toString()}
;
89 |
90 | return (
91 |
99 | );
100 | }
101 |
102 | // TODO rethink about this function
103 | function getKasegiData(kasegiData, kasegiComparedSkill, type) {
104 | const sortByCountAndSkill = (a, b) => {
105 | if (a.count !== b.count) {
106 | return b.count - a.count;
107 | } else if (b.averageSkill - a.averageSkill) {
108 | return b.averageSkill - a.averageSkill;
109 | }
110 | };
111 |
112 | const processData = skillData => (data, index) => {
113 | const { name, diff, part, diffValue, averageSkill } = data;
114 |
115 | let displayedDiff = `${diffValue.toFixed(2)} ${diff}`;
116 | if (part !== "D") {
117 | displayedDiff = `${displayedDiff}-${part}`;
118 | }
119 |
120 | const averageAchieve = ((averageSkill / (diffValue * 20)) * 100).toFixed(2) + "%";
121 | const displayedAverageSkill = `${averageSkill.toFixed(2)} (${averageAchieve})`;
122 |
123 | let compare = null;
124 | if (skillData) {
125 | let comparedData = skillData.find(
126 | skillDataItem => skillDataItem.name === name && skillDataItem.diff === diff && skillDataItem.part === part
127 | );
128 | if (comparedData) {
129 | compare = comparedData.skill_value - averageSkill;
130 | if (compare > 0) {
131 | compare = `${compare.toFixed(2)}↑`;
132 | } else if (compare < 0) {
133 | compare = `${compare.toFixed(2).substring(1)}↓`;
134 | } else {
135 | compare = `0.00`;
136 | }
137 | }
138 | }
139 |
140 | return {
141 | index: index + 1,
142 | diff,
143 | displayedDiff,
144 | displayedAverageSkill,
145 | compare,
146 | ...data
147 | };
148 | };
149 |
150 | if (!kasegiData) {
151 | return null;
152 | }
153 |
154 | const { hot: kasegiHot, other: kasegiOther, ...rest } = kasegiData;
155 | const { hot: skillHot, other: skillOther } =
156 | (kasegiComparedSkill && kasegiComparedSkill[type === "d" ? "drumSkill" : "guitarSkill"]) || {};
157 |
158 | return {
159 | ...rest,
160 | hot: kasegiHot && kasegiHot.sort(sortByCountAndSkill).map(processData(skillHot && skillHot.data)),
161 | other: kasegiOther && kasegiOther.sort(sortByCountAndSkill).map(processData(skillOther && skillOther.data))
162 | };
163 | }
164 |
--------------------------------------------------------------------------------
/src/react/KasegiPage/KasegiTable.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactTable from "react-table";
3 | import { FormattedMessage } from "react-intl";
4 | import { Helmet } from "react-helmet";
5 | import styled from "styled-components";
6 |
7 | import useMediaQuery from "@material-ui/core/useMediaQuery";
8 |
9 | function KasegiTable(props) {
10 | const isPC = useMediaQuery("(min-width:742px)");
11 | const getTrProps = (state, rowInfo) => {
12 | const diff = (rowInfo && rowInfo.original && rowInfo.original.diff) || "";
13 | return {
14 | style: {
15 | backgroundColor: {
16 | BAS: "#C7E7FF",
17 | ADV: "#FFFFC7",
18 | EXT: "#FFC7C7",
19 | MAS: "#D8BFF8"
20 | }[diff]
21 | }
22 | };
23 | };
24 |
25 | const getTdProps = (state, rowInfo, column) => {
26 | let style = {
27 | display: "flex",
28 | flexDirection: "column",
29 | justifyContent: "center"
30 | };
31 | let className;
32 | if (rowInfo && rowInfo.row.compare && column.id === "compare") {
33 | if (rowInfo.row.compare.includes("↑")) {
34 | className = "higherThanAverage";
35 | }
36 | if (rowInfo.row.compare.includes("↓")) {
37 | className = "lowerThanAverage";
38 | }
39 | }
40 |
41 | return { className, style };
42 | };
43 |
44 | const getTheadProps = () => ({
45 | style: { height: 30 }
46 | });
47 |
48 | let columns = [
49 | {
50 | Header: "No.",
51 | accessor: "index",
52 | maxWidth: 40
53 | },
54 | {
55 | Header: () => ,
56 | accessor: "name",
57 | minWidth: isPC ? 180 : 150,
58 | style: {
59 | whiteSpace: "unset"
60 | }
61 | },
62 | {
63 | Header: () => ,
64 | accessor: "displayedDiff",
65 | maxWidth: isPC ? 100 : 53,
66 | style: {
67 | whiteSpace: "unset",
68 | textAlign: "center"
69 | }
70 | },
71 | {
72 | Header: () => ,
73 | accessor: "displayedAverageSkill",
74 | maxWidth: isPC ? 140 : 70,
75 | style: {
76 | whiteSpace: "unset",
77 | textAlign: "center"
78 | },
79 | sortMethod: (a, b) => {
80 | let skillA = Number(a.split("(")[0]);
81 | let skillB = Number(b.split("(")[0]);
82 | return skillA - skillB;
83 | }
84 | }
85 | ];
86 |
87 | if (!props.hasComparedSkill || props.mediaQuery === "pc") {
88 | columns = [
89 | ...columns,
90 | {
91 | Header: () => ,
92 | accessor: "count",
93 | maxWidth: isPC ? 70 : 60,
94 | style: {
95 | textAlign: "center"
96 | },
97 | Cell: data => <>{((data.value / props.count) * 100).toFixed(2)}%>
98 | }
99 | ].filter(x => x);
100 | }
101 |
102 | if (props.hasComparedSkill) {
103 | columns = [
104 | ...columns,
105 | {
106 | Header: () => ,
107 | accessor: "compare",
108 | maxWidth: isPC ? 60 : 50,
109 | textAlign: "center",
110 | sortMethod: (a, b, desc) => {
111 | if (!b) return !desc ? -1 : 1;
112 | if (!a) return !desc ? 1 : -1;
113 | let aNum = Number(a.slice(0, a.length - 1));
114 | if (a[a.length - 1] === "↓") aNum = -aNum;
115 | let bNum = Number(b.slice(0, b.length - 1));
116 | if (b[b.length - 1] === "↓") bNum = -bNum;
117 | return aNum - bNum;
118 | }
119 | }
120 | ].filter(x => x);
121 | }
122 |
123 | return (
124 | <>
125 |
126 |
127 |
128 |
129 |
137 |
138 | >
139 | );
140 | }
141 |
142 | const KasegiTableRoot = styled.div`
143 | font-size: 14px;
144 | color: ${({ theme }) => theme.kasegi.table};
145 | background-color: ${({ theme }) => theme.kasegi.tableBg};
146 |
147 | @media (max-width: 742px) {
148 | font-size: 12px;
149 | }
150 | `;
151 |
152 | const stringStyles = `
153 | .higherThanAverage {
154 | color: red
155 | }
156 |
157 | .lowerThanAverage {
158 | color: green
159 | }
160 | `;
161 |
162 | export default KasegiTable;
163 |
--------------------------------------------------------------------------------
/src/react/KasegiPage/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./KasegiPageContainer.jsx";
2 |
--------------------------------------------------------------------------------
/src/react/ListPage/ListPage.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import styled, { withTheme } from "styled-components";
3 | import { Link } from "react-router-dom";
4 | import { Helmet } from "react-helmet";
5 | import { injectIntl, useIntl } from "react-intl";
6 | import ReactTable from "react-table";
7 | import TextField from "@material-ui/core/TextField";
8 | import SearchIcon from "@material-ui/icons/Search";
9 |
10 | import { VERSION_NAME } from "../../constants.js";
11 | import skillColorStyles from "../styles/skillColor.js";
12 | import withMediaQuery from "../withMediaQuery";
13 | import { InputAdornment } from "@material-ui/core";
14 |
15 | const getLevel = (...skills) => {
16 | let skill = Math.max(...skills);
17 | return Math.floor(skill / 500);
18 | };
19 |
20 | function ListPage(props) {
21 | const intl = useIntl();
22 | const { theme } = props;
23 |
24 | const [searchText, setSearchText] = useState("");
25 |
26 | const getTrProps = () => ({
27 | style: {
28 | color: theme.list.tableContent,
29 | backgroundColor: theme.list.tableContentBg
30 | }
31 | });
32 |
33 | const getTdProps = (state, rowInfo, column) => {
34 | let style;
35 | if (rowInfo) {
36 | switch (column.id) {
37 | case "guitarSkillPoint":
38 | case "drumSkillPoint":
39 | case "totalSkillPoint":
40 | style = { textAlign: "center" };
41 | break;
42 | default:
43 | }
44 | }
45 |
46 | return { style };
47 | };
48 |
49 | const { locale, version } = props.match.params;
50 |
51 | const fullVersionName = "GITADORA " + VERSION_NAME[version];
52 |
53 | let columns = [
54 | {
55 | Header: "",
56 | accessor: "index",
57 | maxWidth: props.mediaQuery === "sp" ? 40 : 48,
58 | Cell: ({ viewIndex, page, pageSize }) => {
59 | return page * pageSize + viewIndex + 1;
60 | }
61 | },
62 | {
63 | Header: "Name",
64 | accessor: "playerName",
65 | minWidth: 144
66 | },
67 | {
68 | Header: "Guitar",
69 | accessor: "guitarSkillPoint",
70 | maxWidth: props.mediaQuery === "sp" ? 70 : 90,
71 | sortMethod: (a, b) => Number(a) - Number(b),
72 | Cell: ({ row }) => (
73 |
77 | {(row.guitarSkillPoint && row.guitarSkillPoint.toFixed(2)) || "0.00"}
78 |
79 | )
80 | },
81 | {
82 | Header: "Drum",
83 | accessor: "drumSkillPoint",
84 | maxWidth: props.mediaQuery === "sp" ? 70 : 90,
85 | sortMethod: (a, b) => Number(a) - Number(b),
86 | Cell: ({ row }) => (
87 |
88 | {(row.drumSkillPoint && row.drumSkillPoint.toFixed(2)) || "0.00"}
89 |
90 | )
91 | },
92 | {
93 | Header: "Total",
94 | accessor: "totalSkillPoint",
95 | maxWidth: props.mediaQuery === "sp" ? 70 : 90,
96 | sortMethod: (a, b) => Number(a) - Number(b),
97 | Cell: ({ row }) => (
98 |
102 | {(row.totalSkillPoint && row.totalSkillPoint.toFixed(2)) || "0.00"}
103 |
104 | )
105 | },
106 | {
107 | Header: "Last Update",
108 | accessor: "updateDate",
109 | width: 145
110 | }
111 | ];
112 |
113 | if (props.isAdmin) {
114 | columns = [
115 | ...columns,
116 | {
117 | Header: "Count",
118 | accessor: "updateCount",
119 | minWidth: 30
120 | }
121 | ];
122 | }
123 |
124 | const {
125 | data,
126 | intl: { formatMessage }
127 | } = props;
128 |
129 | return (
130 | <>
131 |
132 | {`${formatMessage({
133 | id: "list"
134 | })} | ${fullVersionName} | Gitadora Skill Viewer`}
135 |
136 |
137 |
138 | {data && (
139 |
140 | {fullVersionName}
141 | setSearchText(e.target.value)}
144 | fullWidth
145 | placeholder={intl.formatMessage({ id: "list.searchPlaceholder" })}
146 | margin="dense"
147 | InputProps={{
148 | startAdornment: (
149 |
150 |
151 |
152 | )
153 | }}
154 | />
155 |
156 | x.playerName.includes(searchText))}
158 | columns={columns}
159 | defaultPageSize={100}
160 | pageSizeOptions={[5, 100, 200, 500, 1000]}
161 | getTrProps={getTrProps}
162 | getTdProps={getTdProps}
163 | defaultSortDesc
164 | defaultSorted={[
165 | {
166 | id: "totalSkillPoint",
167 | desc: true
168 | }
169 | ]}
170 | />
171 |
172 |
173 | )}
174 | >
175 | );
176 | }
177 |
178 | const TableDiv = styled.div`
179 | font-size: 14px;
180 | color: ${({ theme }) => theme.list.table};
181 | background-color: ${({ theme }) => theme.list.tableBg};
182 |
183 | @media (max-width: 742px) {
184 | font-size: 12px;
185 | }
186 | `;
187 |
188 | const ListTableContainer = styled.div`
189 | max-width: ${({ isAdmin }) => (isAdmin ? 800 : 700)}px;
190 | `;
191 |
192 | const Title = styled.h2`
193 | text-align: center;
194 | `;
195 |
196 | const stringStyles = skillColorStyles;
197 |
198 | const SearchField = styled(TextField)`
199 | & > .MuiInputBase-root {
200 | color: ${({ theme }) => theme.main};
201 | }
202 | margin-left: 8px;
203 | margin-right: 8px;
204 | width: 100%;
205 | `;
206 |
207 | export default withTheme(withMediaQuery(injectIntl(ListPage)));
208 |
--------------------------------------------------------------------------------
/src/react/ListPage/ListPageContainer.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import gql from "graphql-tag";
3 | import { useQuery } from "@apollo/react-hooks";
4 | import LinearProgress from "@material-ui/core/LinearProgress";
5 |
6 | import ListPage from "./ListPage.jsx";
7 |
8 | const GET_LISTS = gql`
9 | query UserList($version: Version) {
10 | users(version: $version) {
11 | playerId
12 | playerName
13 | updateDate
14 | updateCount
15 | drumSkillPoint
16 | guitarSkillPoint
17 | }
18 | }
19 | `;
20 |
21 | export default function ListPageContainer(props) {
22 | const { data, loading, error } = useQuery(GET_LISTS, {
23 | variables: {
24 | version: props.match.params.version
25 | }
26 | });
27 |
28 | if (loading) return ;
29 | if (error) return ERROR: {error.toString()}
;
30 |
31 | const listData = data.users
32 | .map(user => ({
33 | ...user,
34 | totalSkillPoint: user.drumSkillPoint + user.guitarSkillPoint
35 | }))
36 | .sort((a, b) => b.totalSkillPoint - a.totalSkillPoint);
37 |
38 | return (
39 |
46 | );
47 | }
48 |
--------------------------------------------------------------------------------
/src/react/ListPage/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./ListPageContainer.jsx";
2 |
--------------------------------------------------------------------------------
/src/react/SharedSongsPage/SearchBox.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 | import TextField from "@material-ui/core/TextField";
4 | import InputAdornment from "@material-ui/core/InputAdornment";
5 | import IconButton from "@material-ui/core/IconButton";
6 | import SearchIcon from "@material-ui/icons/Search";
7 | import Paper from "@material-ui/core/Paper";
8 | import MenuItem from "@material-ui/core/MenuItem";
9 | import Downshift from "downshift";
10 |
11 | function stateReducer(state, changes) {
12 | // this prevents the input from being clear on blur
13 | switch (changes.type) {
14 | case Downshift.stateChangeTypes.blurInput:
15 | case Downshift.stateChangeTypes.touchEnd:
16 | case Downshift.stateChangeTypes.mouseUp:
17 | return { ...state, isOpen: false };
18 | default:
19 | return changes;
20 | }
21 | }
22 |
23 | function SearchBox(props) {
24 | const { suggestionsData } = props;
25 |
26 | let suggestions =
27 | suggestionsData &&
28 | suggestionsData.sharedSongSuggestions &&
29 | suggestionsData.sharedSongSuggestions.filter(suggestion => suggestion.includes(props.inputValue));
30 |
31 | return (
32 | {
36 | props.onChangeInputValue(value);
37 | props.onFetchSharedSongs(props.inputValue);
38 | }}
39 | inputValue={props.inputValue}
40 | onInputValueChange={value => props.onChangeInputValue(value)}
41 | >
42 | {({ getRootProps, getInputProps, getItemProps, isOpen, openMenu }) => {
43 | return (
44 |
49 |
58 | props.onFetchSharedSongs(props.inputValue)}>
59 |
60 |
61 |
62 | )
63 | }}
64 | {...getInputProps({
65 | onFocus: openMenu
66 | })}
67 | />
68 | {isOpen && suggestions && (
69 |
70 | {suggestions.slice(0, 20).map((item, index) => (
71 |
80 | ))}
81 |
82 | )}
83 |
84 | );
85 | }}
86 |
87 | );
88 | }
89 |
90 | const Container = styled.div`
91 | display: flex;
92 | flex-wrap: wrap;
93 | position: relative;
94 | margin: 20px 10px;
95 | `;
96 |
97 | const StyledPaper = styled(Paper)`
98 | position: absolute;
99 | width: 100%;
100 | top: 52px;
101 | max-height: 240px;
102 | overflow: scroll;
103 | `;
104 |
105 | export default SearchBox;
106 |
--------------------------------------------------------------------------------
/src/react/SharedSongsPage/SharedSongsPage.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { Link } from "react-router-dom";
3 | import Divider from "@material-ui/core/Divider";
4 | import styled from "styled-components";
5 | import { FormattedMessage, FormattedHTMLMessage, injectIntl } from "react-intl";
6 | import { Helmet } from "react-helmet";
7 | import CircularProgress from "@material-ui/core/CircularProgress";
8 |
9 | import SlideToggle from "../SlideToggle.jsx";
10 | import SearchBox from "./SearchBox.jsx";
11 | import { CURRENT_VERSION } from "../../constants";
12 |
13 | function SharedSongsPage(props) {
14 | const [inputValue, setInputValue] = useState("");
15 |
16 | const {
17 | match: {
18 | params: { locale, type }
19 | },
20 | intl: { formatMessage },
21 | sharedSongData,
22 | sharedSongsLoading
23 | } = props;
24 |
25 | const { sharedSongs } = sharedSongData || {};
26 |
27 | return (
28 | <>
29 |
30 |
31 |
32 | {formatMessage({
33 | id: "sharedSongs.title"
34 | })}{" "}
35 | |{type === "d" ? " Drummania" : " GuitarFreaks"} | Gitadora Skill Viewer
36 |
37 |
43 |
44 |
45 | {type === "g" && (
46 | <>
47 | GuitarFreaks/
48 | Drummania
49 | >
50 | )}
51 | {type === "d" && (
52 | <>
53 | GuitarFreaks
54 | /Drummania
55 | >
56 | )}
57 |
58 |
59 |
60 | {type === "d" ? " - Drummania" : " - GuitarFreaks"}
61 |
62 |
63 |
64 |
71 | {sharedSongsLoading && }
72 | {sharedSongs && sharedSongs.length === 0 && (
73 |
74 |
75 |
76 | )}
77 | {sharedSongs &&
78 | sharedSongs.length > 0 &&
79 | sharedSongs.map(({ playerId, playerName, gitadoraId, sharedSongs, updateDate }, index) => {
80 | return (
81 |
82 |
83 |
84 | {playerName}
85 |
86 | ギタドラID: {gitadoraId}
87 | {type === "g" && (
88 |
89 | ギター
90 |
91 | {sharedSongs.g.map(song => (
92 |
93 | {song}
94 |
95 | ))}
96 |
97 |
98 | )}
99 | {type === "d" && (
100 |
101 | ドラム
102 |
103 | {sharedSongs.d.map(song => (
104 |
105 | {song}
106 |
107 | ))}
108 |
109 |
110 | )}
111 | Updated at:{updateDate}
112 |
113 | {index !== sharedSongs.length - 1 && }
114 |
115 | );
116 | })}
117 |
118 | }>
119 |
120 | -
121 |
122 |
123 | -
124 | {
129 | return (
130 |
137 | {msg}
138 |
139 | );
140 | },
141 | /* eslint-disable-next-line react/display-name */
142 | strong: msg => {msg}
143 | }}
144 | />
145 |
146 | -
147 |
148 |
149 |
150 |
151 |
152 | }>
153 |
154 | (
159 |
164 | {msg}
165 |
166 | ),
167 | /* eslint-disable-next-line react/display-name */
168 | strong: msg => {msg}
169 | }}
170 | />
171 |
172 | {msg}
177 | }}
178 | />
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 | }>
188 |
189 |
190 |
191 | >
192 | );
193 | }
194 |
195 | const Title = styled.h1`
196 | font-size: 19px;
197 | `;
198 |
199 | const SearchArea = styled.div`
200 | @media (min-width: 742px) {
201 | max-width: 600px;
202 | }
203 | `;
204 |
205 | const LoadingCircle = styled(CircularProgress)`
206 | position: relative;
207 | left: calc(50% - 30px);
208 | `;
209 |
210 | const ResultRoot = styled.div`
211 | display: flex;
212 | flex-wrap: wrap;
213 | padding: 10px 20px;
214 | `;
215 |
216 | const HalfLine = styled.span`
217 | flex: 1 50%;
218 | `;
219 |
220 | const OneLine = styled.span`
221 | flex: 1 100%;
222 | `;
223 |
224 | const SongName = styled.li`
225 | font-weight: ${props => (props.searched ? "bold" : "normal")};
226 | `;
227 |
228 | const Paragraph = styled.p`
229 | margin: 1em;
230 | `;
231 |
232 | const Image = styled.img`
233 | width: 100%;
234 |
235 | @media (min-width: 742px) {
236 | max-width: 600px;
237 | }
238 | `;
239 |
240 | export default injectIntl(SharedSongsPage);
241 |
--------------------------------------------------------------------------------
/src/react/SharedSongsPage/SharedSongsPageContainer.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import gql from "graphql-tag";
3 | import { useLazyQuery } from "@apollo/react-hooks";
4 |
5 | import SharedSongsPage from "./SharedSongsPage.jsx";
6 |
7 | const FETCH_SHARED_SONGS = gql`
8 | query FetchSharedSongs($input: String, $type: GameType) {
9 | sharedSongs(input: $input, type: $type) {
10 | playerId
11 | playerName
12 | gitadoraId
13 | updateDate
14 | sharedSongs {
15 | g
16 | d
17 | }
18 | }
19 | }
20 | `;
21 |
22 | const FETCH_SUGGSTIONS = gql`
23 | query FetchSuggestions($type: GameType) {
24 | sharedSongSuggestions(type: $type)
25 | }
26 | `;
27 |
28 | function SharedSongsPageContainer(props) {
29 | const [fetchSharedSongs, { data: sharedSongsData, loading: sharedSongsLoading }] = useLazyQuery(FETCH_SHARED_SONGS);
30 |
31 | const [fetchSuggestions, { data: suggestionsData }] = useLazyQuery(FETCH_SUGGSTIONS);
32 |
33 | const { type } = props.match.params;
34 |
35 | useEffect(() => {
36 | fetchSuggestions({
37 | variables: { type }
38 | });
39 | }, [type]);
40 |
41 | useEffect(() => {
42 | localStorage.setItem("sharedSongsPage", type);
43 | }, [type]);
44 |
45 | useEffect(() => {
46 | const {
47 | match: {
48 | url,
49 | params: { locale }
50 | }
51 | } = props;
52 |
53 | // eslint-disable-next-line no-unused-vars
54 | var disqus_config = function() {
55 | this.page.url = `http://gsv.fun/${locale}${url.substring(3)}`;
56 | this.page.identifier = "sharedSongs - " + type;
57 | };
58 |
59 | (function() {
60 | var d = document,
61 | s = d.createElement("script");
62 | s.src = "https://gsv-fun.disqus.com/embed.js";
63 | s.setAttribute("data-timestamp", +new Date());
64 | (d.head || d.body).appendChild(s);
65 | })();
66 | }, [type]);
67 |
68 | return (
69 |
75 | fetchSharedSongs({
76 | variables: { input, type }
77 | })
78 | }
79 | />
80 | );
81 | }
82 |
83 | export default SharedSongsPageContainer;
84 |
--------------------------------------------------------------------------------
/src/react/SharedSongsPage/index.js:
--------------------------------------------------------------------------------
1 | import SharedSongsPageContainer from "./SharedSongsPageContainer.jsx";
2 |
3 | export default SharedSongsPageContainer;
4 |
--------------------------------------------------------------------------------
/src/react/SkillPage/SavedSkillPageContainer.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import gql from "graphql-tag";
3 | import { useQuery } from "@apollo/react-hooks";
4 | import LinearProgress from "@material-ui/core/LinearProgress";
5 |
6 | import SkillPage from "./SkillPage.jsx";
7 |
8 | const GET_SKILL = gql`
9 | fragment SkillRecord on SkillRecord {
10 | name
11 | part
12 | diff
13 | skill_value
14 | achive_value
15 | diff_value
16 | }
17 |
18 | fragment HalfSkillTable on HalfSkillTable {
19 | point
20 | data {
21 | ...SkillRecord
22 | }
23 | }
24 |
25 | query SavedSkill($skillId: Int, $version: Version, $type: GameType) {
26 | savedSkill(skillId: $skillId, type: $type, version: $version) {
27 | skillId
28 | playerId
29 | playerName
30 | updateDate
31 | type
32 | skillPoint
33 | skill {
34 | hot {
35 | ...HalfSkillTable
36 | }
37 | other {
38 | ...HalfSkillTable
39 | }
40 | }
41 | }
42 | }
43 | `;
44 |
45 | export default function SavedSkillPageContainer(props) {
46 | const { skillId, version } = props.match.params;
47 | const { data, loading, error } = useQuery(GET_SKILL, {
48 | variables: {
49 | skillId: parseInt(skillId),
50 | version
51 | }
52 | });
53 |
54 | if (loading) return ;
55 | if (error) return ERROR: {error.toString()}
;
56 |
57 | return ;
58 | }
59 |
--------------------------------------------------------------------------------
/src/react/SkillPage/SkillPageContainer.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import gql from "graphql-tag";
3 | import { useMutation, useQuery } from "@apollo/react-hooks";
4 | import LinearProgress from "@material-ui/core/LinearProgress";
5 |
6 | import { OLD_NAME_MAP, CURRENT_VERSION } from "../../constants";
7 | import queryParser from "../../modules/query";
8 | import SkillPage from "./SkillPage.jsx";
9 | import { injectIntl } from "react-intl";
10 |
11 | const GET_SKILL = gql`
12 | fragment SkillRecord on SkillRecord {
13 | name
14 | part
15 | diff
16 | skill_value
17 | achive_value
18 | diff_value
19 | }
20 |
21 | fragment HalfSkillTable on HalfSkillTable {
22 | point
23 | data {
24 | ...SkillRecord
25 | }
26 | }
27 |
28 | fragment SkillTable on SkillTable {
29 | hot {
30 | ...HalfSkillTable
31 | }
32 | other {
33 | ...HalfSkillTable
34 | }
35 | }
36 |
37 | query User($playerId: Int, $version: Version, $type: GameType, $savedSkillId: Int, $rivalPlayerId: Int) {
38 | user(playerId: $playerId, version: $version) {
39 | version
40 | playerId
41 | playerName
42 | updateDate
43 | updateCount
44 | drumSkill {
45 | ...SkillTable
46 | }
47 | guitarSkill {
48 | ...SkillTable
49 | }
50 | }
51 |
52 | savedSkills(playerId: $playerId, type: $type, version: $version) {
53 | skillId
54 | playerName
55 | updateDate
56 | skillPoint
57 | }
58 |
59 | savedSkill(skillId: $savedSkillId, type: $type, version: $version) {
60 | skillId
61 | skill {
62 | ...SkillTable
63 | }
64 | }
65 |
66 | rival: user(playerId: $rivalPlayerId, version: $version) {
67 | playerId
68 | playerName
69 | updateDate
70 | drumSkill {
71 | ...SkillTable
72 | }
73 | guitarSkill {
74 | ...SkillTable
75 | }
76 | }
77 | }
78 | `;
79 |
80 | const SAVE_SKILL = gql`
81 | mutation SaveSkill($version: Version, $data: SimpleUserInput, $playerId: Int, $type: GameType) {
82 | saveSkill(version: $version, data: $data, playerId: $playerId, type: $type)
83 | }
84 | `;
85 |
86 | const omitTypename = (key, value) => (key === "__typename" ? undefined : value);
87 |
88 | function SkillPageContainer(props) {
89 | const { locale, playerId, version, type } = props.match.params;
90 | const query = queryParser(props.location.search);
91 | const {
92 | intl: { formatMessage }
93 | } = props;
94 |
95 | const { data, loading, error } = useQuery(GET_SKILL, {
96 | variables: {
97 | playerId: parseInt(playerId),
98 | version,
99 | type,
100 | // ignore comparing with savedSkill while comparing with rival
101 | savedSkillId: query.r ? null : parseInt(query.c),
102 | rivalPlayerId: parseInt(query.r)
103 | }
104 | });
105 |
106 | const [saveSkill] = useMutation(SAVE_SKILL, {
107 | onCompleted: data => {
108 | if (data.saveSkill >= 0) {
109 | location.href = `/${locale}/${version}/${data.saveSkill}/p`;
110 | } else {
111 | alert(formatMessage({ id: "skill.alreadySaved" }));
112 | }
113 | }
114 | });
115 |
116 | useEffect(() => {
117 | const gsvId = query.setLocalStorage;
118 |
119 | if (gsvId) {
120 | if (version === CURRENT_VERSION) {
121 | localStorage.setItem("gsvId", gsvId);
122 | localStorage.setItem("gsvName", data.user.playerName);
123 | }
124 | window.history.pushState("", "", window.location.href.split("?")[0]);
125 | }
126 | }, []);
127 |
128 | const handleSaveSkill = () => {
129 | return saveSkill({
130 | variables: {
131 | playerId: parseInt(playerId),
132 | version,
133 | type,
134 | data: {
135 | playerName: data.user.playerName,
136 | updateDate: data.user.updateDate,
137 | drumSkill: JSON.parse(JSON.stringify(data.user.drumSkill), omitTypename),
138 | guitarSkill: JSON.parse(JSON.stringify(data.user.guitarSkill), omitTypename)
139 | }
140 | }
141 | });
142 | };
143 |
144 | if (loading) return ;
145 | if (error) return ERROR: {error.toString()}
;
146 |
147 | return (
148 |
156 | );
157 | }
158 |
159 | function getSkillData(skill, skillComparedSkill, type) {
160 | if (!skill) return null;
161 | if (!skillComparedSkill) {
162 | const result = type === "d" ? skill.drumSkill : skill.guitarSkill;
163 | return {
164 | ...skill,
165 | skill: result
166 | };
167 | } else {
168 | const result = type === "d" ? skill.drumSkill : skill.guitarSkill;
169 | const old = skillComparedSkill.skill;
170 |
171 | result.hot = compareSkillHalf(result.hot, old.hot);
172 | result.other = compareSkillHalf(result.other, old.other);
173 |
174 | const skillPointDiff = (
175 | Number(result.hot.point || 0) +
176 | Number(result.other.point || 0) -
177 | Number(old.hot.point || 0) -
178 | Number(old.other.point || 0)
179 | ).toFixed(2);
180 |
181 | return {
182 | ...skill,
183 | skill: result,
184 | skillPointDiff
185 | };
186 | }
187 | }
188 |
189 | function compareSkillHalf(current, old) {
190 | let result = Object.assign({}, current);
191 |
192 | if (!current.data || !old.data) {
193 | return result;
194 | }
195 |
196 | if (result) {
197 | result.data.forEach(item => {
198 | let newSkillFlag = true;
199 | for (let i = 0; i < old.data.length; i++) {
200 | if (old.data[i].name === item.name || OLD_NAME_MAP[old.data[i].name] === item.name) {
201 | newSkillFlag = false;
202 | const newSkill = Number(item.skill_value);
203 | const oldSkill = Number(old.data[i].skill_value);
204 | if (newSkill > oldSkill) {
205 | const sub = (newSkill - oldSkill).toFixed(2);
206 | item.compare = `${sub}↑`;
207 | }
208 | break;
209 | }
210 | }
211 | if (newSkillFlag) {
212 | item.compare = "New!";
213 | }
214 | });
215 | }
216 |
217 | return result;
218 | }
219 |
220 | export default injectIntl(SkillPageContainer);
221 |
--------------------------------------------------------------------------------
/src/react/SkillPage/SkillTable.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 | import { FormattedMessage } from "react-intl";
4 |
5 | class SkillTable extends React.Component {
6 | getRank = value => {
7 | if (value.substr(0, 3) == "100") {
8 | return "SS";
9 | } else {
10 | var value_int = parseInt(value.substr(0, 2));
11 | if (value_int > 94) {
12 | return "SS";
13 | } else if (value_int > 79) {
14 | return "S";
15 | } else if (value_int > 72) {
16 | return "A";
17 | } else if (value_int > 62) {
18 | return "B";
19 | } else {
20 | return "C";
21 | }
22 | }
23 | };
24 |
25 | getDiff = (item, type) => {
26 | return type === "g"
27 | ? `${item.diff_value.toFixed(2)} ${item.diff}-${item.part}`
28 | : `${item.diff_value.toFixed(2)} ${item.diff}`;
29 | };
30 |
31 | render() {
32 | const { data, rivalData, caption, type, hasComparedSkill, ...rest } = this.props;
33 |
34 | let combinedData = data.map((item, index) => ({
35 | index: index + 1,
36 | ...item
37 | }));
38 |
39 | if (rivalData) {
40 | rivalData.forEach(rivalItem => {
41 | let sameItem = combinedData.find(item => {
42 | return (
43 | item.name === rivalItem.name &&
44 | item.diff === rivalItem.diff &&
45 | item.diff_value === rivalItem.diff_value &&
46 | item.part === rivalItem.part
47 | );
48 | });
49 |
50 | if (sameItem) {
51 | sameItem.rivalAchieveValue = rivalItem.achive_value;
52 | sameItem.rivalSkillValue = rivalItem.skill_value;
53 | } else {
54 | combinedData.push({ index: "", ...rivalItem });
55 | }
56 | });
57 | }
58 |
59 | return (
60 |
61 | {caption}
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 | {hasComparedSkill && | }
78 |
79 |
80 |
81 | {combinedData
82 | .sort((a, b) => b.skill_value - a.skill_value)
83 | .map(item => (
84 |
85 | {item.index}
86 | {item.name}
87 | {this.getDiff(item, type)}
88 |
89 | {`${item.achive_value} (${this.getRank(item.achive_value)})`}
90 | {item.rivalAchieveValue && (
91 | <>
92 |
93 | = item.rivalSkillValue}>
94 | {`${item.rivalAchieveValue} (${this.getRank(item.rivalAchieveValue)})`}
95 |
96 | >
97 | )}
98 |
99 |
100 | {item.skill_value.toFixed(2)}
101 | {item.rivalSkillValue && (
102 | <>
103 |
104 | = item.rivalSkillValue}>
105 | {item.rivalSkillValue.toFixed(2)}
106 |
107 | >
108 | )}
109 |
110 | {hasComparedSkill && {item.compare}}
111 |
112 | ))}
113 |
114 |
115 | );
116 | }
117 | }
118 |
119 | const SkillTableRoot = styled.table`
120 | font-size: 14px;
121 | margin-top: 10px;
122 | max-width: 700px;
123 | opacity: ${({ theme }) => theme.skill.tableOpacity};
124 |
125 | @media (max-width: 742px) {
126 | max-width: 500px;
127 | font-size: 10px;
128 | }
129 |
130 | & > caption {
131 | font-size: 14px;
132 |
133 | @media (max-width: 742px) {
134 | font-size: 12px;
135 | }
136 | }
137 | `;
138 |
139 | const SkillTableTh = styled.th`
140 | color: ${({ theme }) => theme.skill.table};
141 | background-color: #5882fa;
142 | height: 22px;
143 | `;
144 |
145 | const SkillTableTr = styled.tr`
146 | color: ${({ theme }) => theme.skill.table};
147 | height: 24px;
148 | background-color: ${({ diff, isRivalData, theme }) => {
149 | if (isRivalData) return "darkgrey";
150 |
151 | return (
152 | {
153 | BAS: "#C7E7FF",
154 | ADV: "#FFFFC7",
155 | EXT: "#FFC7C7",
156 | MAS: "#D8BFF8"
157 | }[diff] + theme.skill.tableBgHexOpacity
158 | );
159 | }};
160 |
161 | @media (max-width: 742px) {
162 | height: 18px;
163 | }
164 | `;
165 |
166 | const SkillTableTd = styled.td`
167 | text-align: center;
168 | padding: 0 10px;
169 | white-space: nowrap;
170 |
171 | @media (max-width: 742px) {
172 | padding: 0 5px;
173 | }
174 | `;
175 |
176 | const SkillTableNoTd = styled(SkillTableTd)`
177 | padding: 0 5px;
178 |
179 | @media (max-width: 742px) {
180 | padding: 0 3px;
181 | }
182 | `;
183 |
184 | const SkillTableNameTd = styled(SkillTableTd)`
185 | width: 100%;
186 | max-width: 400px;
187 | text-align: left;
188 | white-space: normal;
189 | padding: 0 5px;
190 |
191 | @media (max-width: 742px) {
192 | padding: 0 3px;
193 | }
194 | `;
195 |
196 | const SkillTableCompareTd = styled.td`
197 | text-align: left;
198 | color: ${({ theme }) => theme.main};
199 | background-color: ${({ theme }) => theme.mainBg};
200 | font-size: 12px;
201 |
202 | @media (max-width: 742px) {
203 | font-size: 10px;
204 | }
205 | `;
206 |
207 | const RivalData = styled.span`
208 | color: ${({ win }) => (win ? "green" : "red")};
209 | `;
210 |
211 | export default SkillTable;
212 |
--------------------------------------------------------------------------------
/src/react/SkillPage/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./SkillPageContainer.jsx";
2 | export { default as SavedSkillPageContainer } from "./SavedSkillPageContainer.jsx";
3 |
--------------------------------------------------------------------------------
/src/react/SlideToggle.jsx:
--------------------------------------------------------------------------------
1 | import React, { useRef, useState } from "react";
2 | import styled from "styled-components";
3 |
4 | function SlideToggle(props) {
5 | const [open, setOpen] = useState(props.defaultOpen ?? false);
6 | const toggleDivEl = useRef(null);
7 |
8 | // TODO stop using $
9 | const handleToggleDiv = () => {
10 | // eslint-disable-next-line no-undef
11 | $(toggleDivEl.current).slideToggle(400, () => {
12 | setOpen(!open);
13 | });
14 | };
15 |
16 | return (
17 |
18 |
19 | {props.title}
20 |
21 |
22 | {props.children}
23 |
24 |
25 | );
26 | }
27 |
28 | const SlideToggleDiv = styled.div`
29 | margin-bottom: 5px;
30 | `;
31 |
32 | const Title = styled.h2`
33 | height: 35px;
34 | line-height: 35px;
35 | font-weight: bold;
36 | margin: 0;
37 | color: ${({ theme }) => theme.index.subHeader};
38 | background-color: ${({ theme }) => theme.index.subHeaderBg};
39 | cursor: pointer;
40 | padding: 0 4px;
41 |
42 | @media (max-width: 742px) {
43 | height: 30px;
44 | line-height: 30px;
45 | }
46 | `;
47 |
48 | export default SlideToggle;
49 |
--------------------------------------------------------------------------------
/src/react/UserVoicePage.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { FormattedMessage } from "react-intl";
3 |
4 | class UserVoicePage extends React.Component {
5 | componentDidMount() {
6 | const {
7 | match: {
8 | url,
9 | params: { locale }
10 | }
11 | } = this.props;
12 |
13 | // eslint-disable-next-line no-unused-vars
14 | var disqus_config = function() {
15 | this.page.url = `http://gsv.fun/${locale}${url.substring(3)}`;
16 | this.page.identifier = "uservoice";
17 | };
18 |
19 | (function() {
20 | var d = document,
21 | s = d.createElement("script");
22 | s.src = "https://gsv-fun.disqus.com/embed.js";
23 | s.setAttribute("data-timestamp", +new Date());
24 | (d.head || d.body).appendChild(s);
25 | })();
26 | }
27 |
28 | render() {
29 | const {
30 | match: {
31 | params: { locale }
32 | }
33 | } = this.props;
34 | return (
35 |
36 |
User Voice
37 |
38 |
39 |
40 | {locale === "ja" && (
41 | <>
42 |
既知の不具合
43 |
44 | -
45 | 一部のAndroid Chromeではbookmarklet
46 | scriptが動かないため、スキルデータのアップロードができません。お手数ですがパソコンでのアップロードも試していただければと思います。
47 |
48 |
49 | >
50 | )}
51 |
52 |
53 | );
54 | }
55 | }
56 |
57 | export default UserVoicePage;
58 |
--------------------------------------------------------------------------------
/src/react/styles/skillColor.js:
--------------------------------------------------------------------------------
1 | const skillColorStyles = `.lv0, .lv1 {
2 | background: -webkit-linear-gradient(#FFFFFF, #FFFFFF);
3 | -webkit-background-clip: text;
4 | -webkit-text-fill-color: transparent;
5 | }
6 |
7 | .lv2 {
8 | background: -webkit-linear-gradient(#FF6600, #FF6600);
9 | -webkit-background-clip: text;
10 | -webkit-text-fill-color: transparent;
11 | }
12 |
13 | .lv3 {
14 | background: -webkit-linear-gradient(#FF6600, #FFFFFF);
15 | -webkit-background-clip: text;
16 | -webkit-text-fill-color: transparent;
17 | }
18 |
19 | .lv4 {
20 | background: -webkit-linear-gradient(#FFFF00, #FFFF00);
21 | -webkit-background-clip: text;
22 | -webkit-text-fill-color: transparent;
23 | }
24 |
25 | .lv5 {
26 | background: -webkit-linear-gradient(#FFFF00, #FFFFFF);
27 | -webkit-background-clip: text;
28 | -webkit-text-fill-color: transparent;
29 | }
30 |
31 | .lv6 {
32 | background: -webkit-linear-gradient(#33FF00, #33FF00);
33 | -webkit-background-clip: text;
34 | -webkit-text-fill-color: transparent;
35 | }
36 |
37 | .lv7 {
38 | background: -webkit-linear-gradient(#33FF00, #FFFFFF);
39 | -webkit-background-clip: text;
40 | -webkit-text-fill-color: transparent;
41 | }
42 |
43 | .lv8 {
44 | background: -webkit-linear-gradient(#3366FF, #3366FF);
45 | -webkit-background-clip: text;
46 | -webkit-text-fill-color: transparent;
47 | }
48 |
49 | .lv9 {
50 | background: -webkit-linear-gradient(#3366FF, #FFFFFF);
51 | -webkit-background-clip: text;
52 | -webkit-text-fill-color: transparent;
53 | }
54 |
55 | .lv10 {
56 | background: -webkit-linear-gradient(#FF00FF, #FF00FF);
57 | -webkit-background-clip: text;
58 | -webkit-text-fill-color: transparent;
59 | }
60 |
61 | .lv11 {
62 | background: -webkit-linear-gradient(#FF00FF, #FFFFFF);
63 | -webkit-background-clip: text;
64 | -webkit-text-fill-color: transparent;
65 | }
66 |
67 | .lv12 {
68 | background: -webkit-linear-gradient(#FF0000, #FF0000);
69 | -webkit-background-clip: text;
70 | -webkit-text-fill-color: transparent;
71 | }
72 |
73 | .lv13 {
74 | background: -webkit-linear-gradient(#FF0000, #FFFFFF);
75 | -webkit-background-clip: text;
76 | -webkit-text-fill-color: transparent;
77 | }
78 |
79 | .lv14 {
80 | background: -webkit-linear-gradient(#DD8844, #DD8844);
81 | -webkit-background-clip: text;
82 | -webkit-text-fill-color: transparent;
83 | }
84 |
85 | .lv15 {
86 | background: -webkit-linear-gradient(#C0C0C0, #C0C0C0);
87 | -webkit-background-clip: text;
88 | -webkit-text-fill-color: transparent;
89 | }
90 |
91 | .lv16 {
92 | background: -webkit-linear-gradient(#FFD700, #FFD700);
93 | -webkit-background-clip: text;
94 | -webkit-text-fill-color: transparent;
95 | }
96 |
97 | .lv17, .lv18, .lv19, .lv20 {
98 | background-image: -webkit-gradient( linear, left top, right bottom, color-stop(0.1, #f22), color-stop(0.2, #f2f), color-stop(0.35, #22f), color-stop(0.5, #2ff), color-stop(0.65, #2f2), color-stop(0.8, #ff2) );
99 | color:transparent;
100 | -webkit-background-clip: text;
101 | background-clip: text;
102 | }
103 |
104 | .lv17 > a, .lv18 > a, .lv19 > a, .lv20 > a {
105 | color: unset;
106 | }
107 | `;
108 |
109 | export default skillColorStyles;
110 |
--------------------------------------------------------------------------------
/src/react/useLocalStorage.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 |
3 | export default function useLocalStorage(name, defaultValue) {
4 | const [value, setValue] = useState(defaultValue);
5 |
6 | useEffect(() => {
7 | const storedValue = localStorage.getItem(name);
8 | if (storedValue !== null) {
9 | setValue(storedValue);
10 | }
11 | }, []);
12 |
13 | return value;
14 | }
15 |
--------------------------------------------------------------------------------
/src/react/withMediaQuery.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | function getDisplayName(WrappedComponent) {
4 | return WrappedComponent.displayName || WrappedComponent.name || "Component";
5 | }
6 |
7 | export default function withMediaQuery(WrappedComponent) {
8 | class WithMediaQuery extends React.Component {
9 | constructor(props) {
10 | super(props);
11 |
12 | this.state = {
13 | mediaQuery: "sp"
14 | };
15 | }
16 |
17 | componentDidMount() {
18 | this.handleResize();
19 | window.addEventListener("resize", this.handleResize);
20 | }
21 |
22 | componentWillUnmount() {
23 | window.removeEventListener("resize", this.handleResize);
24 | }
25 |
26 | handleResize = () => {
27 | this.setState({
28 | mediaQuery: window.matchMedia("(max-width: 742px)").matches ? "sp" : "pc"
29 | });
30 | };
31 |
32 | render() {
33 | return ;
34 | }
35 | }
36 | WithMediaQuery.displayName = `WithSubscription(${getDisplayName(WrappedComponent)})`;
37 |
38 | return WithMediaQuery;
39 | }
40 |
--------------------------------------------------------------------------------
/src/resolvers.js:
--------------------------------------------------------------------------------
1 | const pg = require("./modules/pg");
2 | const { CURRENT_VERSION } = require("./constants");
3 |
4 | // Provide resolver functions for your schema fields
5 | module.exports = {
6 | GameType: {
7 | g: "g",
8 | d: "d"
9 | },
10 | Version: {
11 | exchain: "exchain",
12 | matixx: "matixx",
13 | tbre: "tbre",
14 | tb: "tb"
15 | },
16 | Query: {
17 | users: async (_, { version }) => {
18 | const sql = `select "playerId", "version", "cardNumber", "gitadoraId", "playerName", "guitarSkillPoint", "drumSkillPoint", "updateDate", "updateCount" from skill where version='${version}' order by "playerId" asc;`;
19 | const result = await pg.query(sql);
20 |
21 | return result.rows;
22 | },
23 | user: async (_, { playerId, version }) => {
24 | if (playerId == null) return null;
25 | const sql = `select * from skill where version='${version}' and "playerId"=${playerId};`;
26 | const result = await pg.query(sql);
27 | return result.rows[0];
28 | },
29 | kasegi: async (_, { version, type, scope }) => {
30 | const typeFull = {
31 | d: "drum",
32 | g: "guitar"
33 | }[type];
34 |
35 | const sql = `select * from kasegi where version=$$${version}$$ and type=$$${typeFull}$$ and scope=${scope};`;
36 |
37 | const result = await pg.query(sql);
38 | const kasegiResult = result.rows[0];
39 |
40 | if (kasegiResult) {
41 | const kasegiListHot = JSON.parse(kasegiResult.list_hot);
42 | const kasegiListOther = JSON.parse(kasegiResult.list_other);
43 | const count = kasegiResult.count;
44 |
45 | return {
46 | version,
47 | type,
48 | scope,
49 | count,
50 | hot: kasegiListHot,
51 | other: kasegiListOther
52 | };
53 | } else {
54 | return {
55 | version,
56 | type,
57 | scope
58 | };
59 | }
60 | },
61 | kasegiNew: async (_, { version, type, scope }) => {
62 | const typeFull = {
63 | d: "drum",
64 | g: "guitar"
65 | }[type];
66 |
67 | const sql = `select * from kasegi_new where version=$$${version}$$ and type=$$${typeFull}$$ and scope=${scope};`;
68 |
69 | const result = await pg.query(sql);
70 | const kasegiResult = result.rows[0];
71 |
72 | if (kasegiResult) {
73 | const kasegiListHot = JSON.parse(kasegiResult.list_hot);
74 | const kasegiListOther = JSON.parse(kasegiResult.list_other);
75 | const count = kasegiResult.count;
76 |
77 | return {
78 | version,
79 | type,
80 | scope,
81 | count,
82 | hot: kasegiListHot,
83 | other: kasegiListOther
84 | };
85 | } else {
86 | return {
87 | version,
88 | type,
89 | scope
90 | };
91 | }
92 | },
93 | savedSkill: async (_, { skillId, version }) => {
94 | if (!skillId) return null;
95 | const result = await pg.query(`select * from skillp where version='${version}' and "skillId"=${skillId};`);
96 | return result.rows[0];
97 | },
98 | savedSkills: async (_, { playerId, version, type }) => {
99 | const sql = `select "skillId", "playerName", "skillPoint", "updateDate" from skillp where "playerId"=${playerId} and "version"='${version}' and type='${type}' order by "updateDate" asc;`;
100 | const result = await pg.query(sql);
101 | return result.rows;
102 | },
103 | sharedSongSuggestions: async (_, { type }) => {
104 | const result = await pg.query(
105 | `SELECT "songName" FROM shared_songs WHERE type='${type}' AND version='${CURRENT_VERSION}' ORDER BY count desc;`
106 | );
107 | const allSuggestions = result.rows;
108 |
109 | return allSuggestions.map(suggestion => suggestion.songName);
110 | },
111 | sharedSongs: async (_, { input, type }) => {
112 | if (!input) return null;
113 |
114 | pg.query(
115 | `UPDATE shared_songs SET count = count + 1 WHERE "songName"=$$${input}$$ AND version='${CURRENT_VERSION}' AND type='${type}';`
116 | );
117 |
118 | const result = await pg.query(
119 | `select * from skill where version='${CURRENT_VERSION}' and "sharedSongs"->>'${type}' LIKE $$%${input}%$$ ORDER BY "updateDate" DESC LIMIT 20;`
120 | );
121 |
122 | return result.rows;
123 | },
124 | errors: async () => {
125 | const result = await pg.query(`SELECT * FROM errorReports ORDER BY "date" DESC LIMIT 100;`);
126 | return result.rows;
127 | }
128 | },
129 | Mutation: {
130 | upload: async (_, { version, data }) => {
131 | const guitarSkillPoint = (
132 | parseFloat(data.guitarSkill.hot.point) + parseFloat(data.guitarSkill.other.point)
133 | ).toFixed(2);
134 | const drumSkillPoint = (parseFloat(data.drumSkill.hot.point) + parseFloat(data.drumSkill.other.point)).toFixed(2);
135 | const guitarDataStr = JSON.stringify(data.guitarSkill);
136 | const drumDataStr = JSON.stringify(data.drumSkill);
137 | const sharedSongsStr = JSON.stringify(data.sharedSongs) || "{}";
138 |
139 | updateSharedSongList(data.sharedSongs);
140 |
141 | let playerDataRow;
142 |
143 | // try to get player data by gitadora id
144 | if (data.gitadoraId) {
145 | const searchByGitadoraIdResult = await pg.query(
146 | `select * from skill where "gitadoraId" = $$${data.gitadoraId}$$ and version='${version}';`
147 | );
148 |
149 | if (searchByGitadoraIdResult.rows[0]) {
150 | playerDataRow = searchByGitadoraIdResult.rows[0];
151 | }
152 | }
153 |
154 | // try to get player data by card number
155 | if (!playerDataRow) {
156 | const searchByCardNumberResult = await pg.query(
157 | `select * from skill where "cardNumber" = $$${data.cardNumber}$$ and version='${version}';`
158 | );
159 | if (searchByCardNumberResult.rows[0]) {
160 | playerDataRow = searchByCardNumberResult.rows[0];
161 | }
162 | }
163 |
164 | if (playerDataRow) {
165 | await pg.query("BEGIN");
166 |
167 | const { playerId, guitarSkillPoint: oldGuitarSkillPoint, drumSkillPoint: oldDrumSkillPoint } = playerDataRow;
168 |
169 | if (oldGuitarSkillPoint < guitarSkillPoint) await saveSkill({ version, data, playerId, type: "g" });
170 |
171 | if (oldDrumSkillPoint < drumSkillPoint) await saveSkill({ version, data, playerId, type: "d" });
172 |
173 | await pg.query(`
174 | UPDATE skill SET
175 | "playerName" = $$${data.playerName}$$,
176 | "cardNumber" = $$${data.cardNumber}$$,
177 | "gitadoraId" = $$${data.gitadoraId}$$,
178 | "guitarSkillPoint" = $$${guitarSkillPoint}$$,
179 | "drumSkillPoint" = $$${drumSkillPoint}$$,
180 | "guitarSkill" = $$${guitarDataStr}$$::json,
181 | "drumSkill" = $$${drumDataStr}$$::json,
182 | "updateDate" = $$${data.updateDate}$$,
183 | "updateCount" = ${(playerDataRow.updateCount || 1) + 1},
184 | "sharedSongs" = $$${sharedSongsStr}$$::json
185 | WHERE "playerId" = ${playerId} and version = '${version}';`);
186 |
187 | await pg.query("COMMIT");
188 |
189 | return playerId;
190 | } else {
191 | await pg.query("BEGIN");
192 | const idResult = await pg.query(
193 | `SELECT coalesce(max("playerId") + 1,0) as "playerId" FROM skill WHERE version='${version}';`
194 | );
195 |
196 | const playerId = idResult.rows[0].playerId;
197 |
198 | await pg.query(`
199 | INSERT INTO skill VALUES (
200 | ${playerId}, $$${version}$$, $$${data.cardNumber}$$,
201 | $$${data.gitadoraId}$$, $$${data.playerName}$$,
202 | $$${guitarSkillPoint}$$, $$${drumSkillPoint}$$,
203 | $$${guitarDataStr}$$::json, $$${drumDataStr}$$::json,
204 | $$${data.updateDate}$$, 1, $$${sharedSongsStr}$$::json
205 | );
206 | `);
207 |
208 | await saveSkill({ version, data, playerId, type: "g" });
209 | await saveSkill({ version, data, playerId, type: "d" });
210 |
211 | await pg.query("COMMIT");
212 | return playerId;
213 | }
214 | },
215 | postError: async (_, { version, error, date, userAgent }) => {
216 | await pg.query(`INSERT INTO errorReports VALUES (
217 | $$${version}$$ , $$${error}$$, $$${date}$$, $$${userAgent}$$
218 | );`);
219 | return;
220 | },
221 | saveSkill: async (_, { version, data, playerId, type }) => {
222 | return await saveSkill({ version, data, playerId, type });
223 | }
224 | }
225 | };
226 |
227 | async function saveSkill({ version, data, playerId, type }) {
228 | const result = await pg.query(
229 | `SELECT "skillPoint" FROM skillp WHERE version='${version}' AND type='${type}' AND "playerId"='${playerId}'`
230 | );
231 | const maxSkillPoint = Math.max(...result.rows.map(x => Number(x.skillPoint)));
232 |
233 | const guitarSkillPoint = (parseFloat(data.guitarSkill.hot.point) + parseFloat(data.guitarSkill.other.point)).toFixed(
234 | 2
235 | );
236 | const drumSkillPoint = (parseFloat(data.drumSkill.hot.point) + parseFloat(data.drumSkill.other.point)).toFixed(2);
237 |
238 | console.log("maxSkillPoint", maxSkillPoint);
239 | console.log("Number(guitarSkillPoint)", Number(guitarSkillPoint));
240 | console.log("Number(drumSkillPoint)", Number(drumSkillPoint));
241 |
242 | if (type === "g" && maxSkillPoint && maxSkillPoint >= Number(guitarSkillPoint)) {
243 | return -1;
244 | }
245 |
246 | if (type === "d" && maxSkillPoint && maxSkillPoint >= Number(drumSkillPoint)) {
247 | return -1;
248 | }
249 |
250 | const guitarDataStr = JSON.stringify(data.guitarSkill);
251 | const drumDataStr = JSON.stringify(data.drumSkill);
252 |
253 | const skillIdResult = await pg.query(
254 | `SELECT coalesce(max("skillId") + 1,0) as "skillId" FROM skillp WHERE version='${version}';`
255 | );
256 |
257 | const skillId = skillIdResult.rows[0].skillId;
258 |
259 | await pg.query(`
260 | INSERT INTO skillp VALUES (
261 | ${skillId}, $$${version}$$, ${playerId}, $$${data.playerName}$$,
262 | $$${type}$$,
263 | $$${type === "g" ? guitarSkillPoint : drumSkillPoint}$$,
264 | $$${type === "g" ? guitarDataStr : drumDataStr}$$::json,
265 | $$${data.updateDate}$$
266 | );
267 | `);
268 | return skillId;
269 | }
270 |
271 | async function updateSharedSongList(sharedSongs) {
272 | if (!sharedSongs) return;
273 | sharedSongs.d &&
274 | sharedSongs.d.forEach(async songName => {
275 | await pg.query(`INSERT INTO shared_songs VALUES(
276 | $$${songName}$$, $$${CURRENT_VERSION}$$, 'd', 0
277 | ) ON CONFLICT DO NOTHING;`);
278 | });
279 |
280 | sharedSongs.g &&
281 | sharedSongs.g.forEach(async songName => {
282 | await pg.query(`INSERT INTO shared_songs VALUES(
283 | $$${songName}$$, $$${CURRENT_VERSION}$$, 'g', 0
284 | ) ON CONFLICT DO NOTHING;`);
285 | });
286 | }
287 |
--------------------------------------------------------------------------------
/src/schema.js:
--------------------------------------------------------------------------------
1 | const { gql } = require("apollo-server-express");
2 | // Construct a schema, using GraphQL schema language
3 | const typeDefs = gql`
4 | type Query {
5 | users(version: Version): [User]
6 | user(playerId: Int, type: GameType, version: Version): User
7 | kasegi(scope: Int, type: GameType, version: Version): Kasegi
8 | kasegiNew(scope: Int, type: GameType, version: Version): Kasegi
9 | savedSkill(skillId: Int, type: GameType, version: Version): SavedSkill
10 | savedSkills(playerId: Int, type: GameType, version: Version): [SavedSkill]
11 | sharedSongSuggestions(type: GameType): [String]
12 | sharedSongs(input: String, type: GameType): [User]
13 | errors: [Error]
14 | }
15 |
16 | type Mutation {
17 | upload(version: Version, data: UserInput): Int
18 | postError(version: Version, error: String, date: String, userAgent: String): Boolean
19 | saveSkill(version: Version, data: SimpleUserInput, playerId: Int, type: GameType): Int
20 | }
21 |
22 | enum GameType {
23 | g
24 | d
25 | }
26 |
27 | enum Version {
28 | galaxywave
29 | galaxywave_delta
30 | fuzzup
31 | highvoltage
32 | nextage
33 | exchain
34 | matixx
35 | tbre
36 | tb
37 | }
38 |
39 | type User {
40 | version: String
41 | playerId: Int
42 | playerName: String
43 | gitadoraId: String
44 | updateDate: String
45 | updateCount: Float
46 | drumSkillPoint: Float
47 | guitarSkillPoint: Float
48 | drumSkill: SkillTable
49 | guitarSkill: SkillTable
50 | savedSkills: [SavedSkill]
51 | sharedSongs: SharedSongs
52 | }
53 |
54 | type SavedSkill {
55 | skillId: Int
56 | playerId: Int
57 | playerName: String
58 | updateDate: String
59 | type: String
60 | skillPoint: Float
61 | skill: SkillTable
62 | }
63 |
64 | type SkillTable {
65 | hot: HalfSkillTable
66 | other: HalfSkillTable
67 | }
68 |
69 | type HalfSkillTable {
70 | point: Float
71 | data: [SkillRecord]
72 | }
73 |
74 | type SkillRecord {
75 | name: String
76 | part: String
77 | diff: String
78 | skill_value: Float
79 | achive_value: String
80 | diff_value: Float
81 | }
82 |
83 | type Kasegi {
84 | version: Version
85 | type: GameType
86 | scope: Int
87 | count: Int
88 | hot: [KasegiRecord]
89 | other: [KasegiRecord]
90 | }
91 |
92 | type KasegiRecord {
93 | name: String
94 | diff: String
95 | part: String
96 | diffValue: Float
97 | averageSkill: Float
98 | count: Int
99 | averagePlayerSKill: Float
100 | }
101 |
102 | type SharedSongs {
103 | g: [String]
104 | d: [String]
105 | }
106 |
107 | type Error {
108 | version: Version
109 | error: String
110 | date: String
111 | userAgent: String
112 | }
113 |
114 | input SimpleUserInput {
115 | playerName: String
116 | updateDate: String
117 | drumSkill: SkillTableInputNew
118 | guitarSkill: SkillTableInputNew
119 | }
120 |
121 | input UserInput {
122 | cardNumber: String
123 | gitadoraId: String
124 | playerName: String
125 | updateDate: String
126 | drumSkill: SkillTableInput
127 | guitarSkill: SkillTableInput
128 | sharedSongs: SharedSongsInput
129 | }
130 |
131 | input SkillTableInput {
132 | hot: HalfSkillTableInput
133 | other: HalfSkillTableInput
134 | }
135 |
136 | input SkillTableInputNew {
137 | hot: HalfSkillTableInputNew
138 | other: HalfSkillTableInputNew
139 | }
140 |
141 | input HalfSkillTableInput {
142 | point: String
143 | data: [SkillRecordInput]
144 | }
145 |
146 | input HalfSkillTableInputNew {
147 | point: Float
148 | data: [SkillRecordInputNew]
149 | }
150 |
151 | input SkillRecordInput {
152 | name: String
153 | part: String
154 | diff: String
155 | skill_value: String
156 | achive_value: String
157 | diff_value: String
158 | }
159 |
160 | input SkillRecordInputNew {
161 | name: String
162 | part: String
163 | diff: String
164 | skill_value: Float
165 | achive_value: String
166 | diff_value: Float
167 | }
168 |
169 | input SharedSongsInput {
170 | d: [String]
171 | g: [String]
172 | }
173 | `;
174 |
175 | module.exports = typeDefs;
176 |
--------------------------------------------------------------------------------
/src/scripts/uploaddata_core.js:
--------------------------------------------------------------------------------
1 | import $ from "jquery";
2 | import "regenerator-runtime/runtime";
3 | import * as Sentry from "@sentry/react";
4 | import { Integrations } from "@sentry/tracing";
5 |
6 | import { APP_VERSION, NO_EAM_PATH_VERSIONS } from "../constants";
7 |
8 | // eslint-disable-next-line
9 | async function main(TARGET_DOMAIN, SCRIPT_DOMAIN, VERSION) {
10 | console.log(`Running script ${APP_VERSION}`);
11 |
12 | Sentry.init({
13 | dsn: "https://6d26c7e100a84ae2ae01f54719c5ca19@o912155.ingest.sentry.io/5848955",
14 | integrations: [new Integrations.BrowserTracing()],
15 |
16 | // Set tracesSampleRate to 1.0 to capture 100%
17 | // of transactions for performance monitoring.
18 | // We recommend adjusting this value in production
19 | tracesSampleRate: 0.1,
20 | environment: "production"
21 | });
22 |
23 | var skill_data = {};
24 |
25 | try {
26 | if (window.location.hostname != "p.eagate.573.jp") {
27 | alert(
28 | "コナミ様のサイト(http://p.eagate.573.jp/)で行ってください。\n\n请在Konami的官方网站(http://p.eagate.573.jp/)上点击书签。\n\nPlease make sure you are on Konami official site(http://p.eagate.573.jp/)."
29 | );
30 | return;
31 | }
32 |
33 | // get profile data
34 | const [profileData] = await Promise.all([getProfileData()]);
35 |
36 | console.log("profileData", profileData);
37 |
38 | if (!profileData.cardNumber) {
39 | alert(
40 | "プレイヤーデータ取得できません。ログインした状態でもう一度試してみてください。\n\n无法取得玩家数据,请检查您是否已经登录。\n\nFailed to fetch player data. Please log in."
41 | );
42 |
43 | return;
44 | }
45 |
46 | var SKILL_URLS = [
47 | `//p.eagate.573.jp/game/gfdm/gitadora_${VERSION}/p${
48 | NO_EAM_PATH_VERSIONS.includes(VERSION) ? "/eam" : ""
49 | }/playdata/skill.html?gtype=gf&stype=0`,
50 | `//p.eagate.573.jp/game/gfdm/gitadora_${VERSION}/p${
51 | NO_EAM_PATH_VERSIONS.includes(VERSION) ? "/eam" : ""
52 | }/playdata/skill.html?gtype=gf&stype=1`,
53 | `//p.eagate.573.jp/game/gfdm/gitadora_${VERSION}/p${
54 | NO_EAM_PATH_VERSIONS.includes(VERSION) ? "/eam" : ""
55 | }/playdata/skill.html?gtype=dm&stype=0`,
56 | `//p.eagate.573.jp/game/gfdm/gitadora_${VERSION}/p${
57 | NO_EAM_PATH_VERSIONS.includes(VERSION) ? "/eam" : ""
58 | }/playdata/skill.html?gtype=dm&stype=1`
59 | ];
60 | var SKILL_LABEL = ["guitar_other", "guitar_hot", "drum_other", "drum_hot"];
61 |
62 | await Promise.all([0, 1, 2, 3].map(index => getSkillData(SKILL_URLS[index], SKILL_LABEL[index])));
63 |
64 | let uploadRes = await $.ajax({
65 | url: `${SCRIPT_DOMAIN}/graphql`,
66 | error: handleAjaxError(`${SCRIPT_DOMAIN}/graphql`),
67 | method: "POST",
68 | contentType: "application/json",
69 | data: JSON.stringify({
70 | query: `
71 | mutation Upload($version: Version, $data: UserInput) {
72 | upload(version: $version, data: $data)
73 | }
74 | `,
75 | variables: {
76 | version: VERSION,
77 | data: {
78 | ...profileData,
79 | guitarSkill: {
80 | hot: skill_data["guitar_hot"],
81 | other: skill_data["guitar_other"]
82 | },
83 | drumSkill: {
84 | hot: skill_data["drum_hot"],
85 | other: skill_data["drum_other"]
86 | },
87 | updateDate: getDate()
88 | }
89 | }
90 | })
91 | });
92 |
93 | if (uploadRes.errors) {
94 | postError(uploadRes.errors);
95 | } else {
96 | window.location = `${TARGET_DOMAIN}/${VERSION}/${uploadRes.data.upload}/g?setLocalStorage=${uploadRes.data.upload}`;
97 | }
98 | } catch (error) {
99 | // unhandled eror
100 | postError(error);
101 | }
102 |
103 | // for passing parameters
104 | async function getSkillData(url, label) {
105 | await $.ajax({
106 | url: url,
107 | error: handleAjaxError(url),
108 | success: function(html) {
109 | var doc = document.implementation.createHTMLDocument("html");
110 | doc.documentElement.innerHTML = html;
111 |
112 | var skill_table = $(doc).find(".skill_table_tb");
113 | var lines = skill_table
114 | .children()
115 | .eq(1)
116 | .children();
117 |
118 | var skill_data_per_page = [];
119 | var skill_point = 0;
120 | for (var i = 0; i < 25; i++) {
121 | try {
122 | var current_line = lines.eq(i);
123 | var name = current_line
124 | .find("a.text_link")
125 | .eq(0)
126 | .text();
127 | // part: G, B, D
128 | var part = current_line
129 | .find("div.music_seq_box > div")
130 | .eq(0)
131 | .attr("class")
132 | .substring(14, 15);
133 | // diff: BAS, ADV, EXT, MAS
134 | var diff = current_line
135 | .find("div.music_seq_box > div")
136 | .eq(1)
137 | .attr("class")
138 | .substring(14, 17);
139 |
140 | var skill_value = current_line.find("td.skill_cell").text();
141 | skill_value = skill_value.substring(0, skill_value.length - 8).trim();
142 | var achive_value = current_line
143 | .find("td.achive_cell")
144 | .text()
145 | .trim();
146 | var diff_value = current_line
147 | .find("td.diff_cell")
148 | .text()
149 | .trim();
150 |
151 | skill_data_per_page.push({
152 | name: name,
153 | part: part,
154 | diff: diff,
155 | skill_value: skill_value,
156 | achive_value: achive_value,
157 | diff_value: diff_value
158 | });
159 | skill_point += parseFloat(skill_value);
160 | } catch (error) {
161 | // when the form is not fully filled, ignore error
162 | break;
163 | }
164 | }
165 | skill_data[label] = {
166 | point: skill_point.toFixed(2),
167 | data: skill_data_per_page
168 | };
169 | }
170 | });
171 | }
172 |
173 | async function getProfileData() {
174 | var PROFILE_URL = `//p.eagate.573.jp/game/gfdm/gitadora_${VERSION}/p${
175 | NO_EAM_PATH_VERSIONS.includes(VERSION) ? "/eam" : ""
176 | }/playdata/profile.html`;
177 |
178 | let profileData = {};
179 | let resHtml = await $.ajax({
180 | url: PROFILE_URL,
181 | error: handleAjaxError(PROFILE_URL)
182 | });
183 |
184 | var doc = document.implementation.createHTMLDocument("html");
185 | doc.documentElement.innerHTML = resHtml;
186 |
187 | var playerName = $(doc)
188 | .find(".profile_name_frame")
189 | .text();
190 |
191 | var cardNumber = "";
192 | var gitadoraId = "";
193 |
194 | if (VERSION === "matixx" || VERSION === "tbre") {
195 | cardNumber = $(doc)
196 | .find(".common_frame_date")
197 | .text()
198 | .substring(10, 26);
199 | } else {
200 | cardNumber = $(doc)
201 | .find("#contents > .maincont > h2")
202 | .text()
203 | .match(/[a-zA-Z0-9]+/);
204 | cardNumber = cardNumber && cardNumber[0];
205 |
206 | gitadoraId = $(doc)
207 | .find("div.common_frame_date")
208 | .text()
209 | .trim();
210 | }
211 |
212 | profileData.playerName = playerName;
213 | profileData.cardNumber = cardNumber;
214 | profileData.gitadoraId = gitadoraId;
215 |
216 | return profileData;
217 | }
218 |
219 | async function postError(error) {
220 | if (!error) return;
221 | console.error(error);
222 |
223 | let errorStr = JSON.stringify(error);
224 | await $.ajax({
225 | url: `${SCRIPT_DOMAIN}/graphql`,
226 | method: "POST",
227 | contentType: "application/json",
228 | data: JSON.stringify({
229 | query: `
230 | mutation PostError($version: Version, $error: String, $date: String, $userAgent: String) {
231 | postError(version: $version, error: $error, date: $date, userAgent: $userAgent)
232 | }
233 | `,
234 | variables: {
235 | version: VERSION,
236 | error: errorStr,
237 | date: getDate(),
238 | userAgent: window.navigator.userAgent
239 | }
240 | }),
241 | error: () => {
242 | alert(
243 | `[failed to report error]\nエラーが発生しました。\n出了点问题。\nYou got an error.\n\n[error message]\n${errorStr}`
244 | );
245 | },
246 | success: () => {
247 | alert(
248 | `[error reported]\nエラーが発生しました。\n出了点问题。\nYou got an error.\n\n[error message]\n${errorStr}`
249 | );
250 | }
251 | });
252 | }
253 |
254 | function handleAjaxError(url) {
255 | return function(request, status) {
256 | console.error(`${request.responseText}\n\nstatus: ${status}`);
257 | postError(`${url}\n\n${request.responseText}\n\nstatus: ${status}`);
258 | };
259 | }
260 | }
261 |
262 | function getDate() {
263 | var date = new Date();
264 | var mm = date.getMinutes();
265 | var hh = date.getHours();
266 | var DD = date.getDate();
267 | var MM = date.getMonth() + 1;
268 | var YYYY = date.getFullYear();
269 |
270 | if (mm < 10) {
271 | mm = `0${mm}`;
272 | }
273 |
274 | if (hh < 10) {
275 | hh = `0${hh}`;
276 | }
277 |
278 | if (DD < 10) {
279 | DD = `0${DD}`;
280 | }
281 |
282 | if (MM < 10) {
283 | MM = `0${MM}`;
284 | }
285 |
286 | return `${YYYY}/${MM}/${DD} ${hh}:${mm}`;
287 | }
288 |
289 | export default main;
290 |
--------------------------------------------------------------------------------
/src/scripts/uploaddata_exchain.js:
--------------------------------------------------------------------------------
1 | import main from "./uploaddata_core";
2 |
3 | var TARGET_DOMAIN = "http://gsv.fun";
4 | var SCRIPT_DOMAIN = "//gitadora-skill-viewer.herokuapp.com";
5 | var VERSION = "exchain";
6 |
7 | main(TARGET_DOMAIN, SCRIPT_DOMAIN, VERSION);
8 |
--------------------------------------------------------------------------------
/src/scripts/uploaddata_exchain_dev.js:
--------------------------------------------------------------------------------
1 | import main from "./uploaddata_core";
2 |
3 | var TARGET_DOMAIN = "//gitadora-skill-viewer-dev.herokuapp.com";
4 | var SCRIPT_DOMAIN = TARGET_DOMAIN;
5 | var VERSION = "exchain";
6 |
7 | alert("Script is running!");
8 | main(TARGET_DOMAIN, SCRIPT_DOMAIN, VERSION);
9 |
--------------------------------------------------------------------------------
/src/scripts/uploaddata_exchain_local.js:
--------------------------------------------------------------------------------
1 | import main from "./uploaddata_core";
2 |
3 | var TARGET_DOMAIN = "http://127.0.0.1:5000";
4 | var SCRIPT_DOMAIN = TARGET_DOMAIN;
5 | var VERSION = "exchain";
6 |
7 | main(TARGET_DOMAIN, SCRIPT_DOMAIN, VERSION);
8 |
--------------------------------------------------------------------------------
/src/scripts/uploaddata_highvoltage.js:
--------------------------------------------------------------------------------
1 | import main from "./uploaddata_core";
2 |
3 | var TARGET_DOMAIN = "http://gsv.fun";
4 | var SCRIPT_DOMAIN = "//gitadora-skill-viewer.herokuapp.com";
5 | var VERSION = "highvoltage";
6 |
7 | main(TARGET_DOMAIN, SCRIPT_DOMAIN, VERSION);
8 |
--------------------------------------------------------------------------------
/src/scripts/uploaddata_highvoltage_dev.js:
--------------------------------------------------------------------------------
1 | import main from "./uploaddata_core";
2 |
3 | var TARGET_DOMAIN = "//gitadora-skill-viewer-dev.herokuapp.com";
4 | var SCRIPT_DOMAIN = TARGET_DOMAIN;
5 | var VERSION = "highvoltage";
6 |
7 | alert("Script is running!");
8 | main(TARGET_DOMAIN, SCRIPT_DOMAIN, VERSION);
9 |
--------------------------------------------------------------------------------
/src/scripts/uploaddata_highvoltage_local.js:
--------------------------------------------------------------------------------
1 | import main from "./uploaddata_core";
2 |
3 | var TARGET_DOMAIN = "http://127.0.0.1:5000";
4 | var SCRIPT_DOMAIN = TARGET_DOMAIN;
5 | var VERSION = "highvoltage";
6 |
7 | main(TARGET_DOMAIN, SCRIPT_DOMAIN, VERSION);
8 |
--------------------------------------------------------------------------------
/src/scripts/uploaddata_latest.js:
--------------------------------------------------------------------------------
1 | import main from "./uploaddata_core";
2 | import { CURRENT_VERSION } from "../constants";
3 |
4 | var TARGET_DOMAIN = "http://gsv.fun";
5 | var SCRIPT_DOMAIN = "//gitadora-skill-viewer.herokuapp.com";
6 | var VERSION = CURRENT_VERSION;
7 |
8 | main(TARGET_DOMAIN, SCRIPT_DOMAIN, VERSION);
9 |
--------------------------------------------------------------------------------
/src/scripts/uploaddata_latest_dev.js:
--------------------------------------------------------------------------------
1 | import main from "./uploaddata_core";
2 | import { CURRENT_VERSION } from "../constants";
3 |
4 | var TARGET_DOMAIN = "//gitadora-skill-viewer-dev.herokuapp.com";
5 | var SCRIPT_DOMAIN = TARGET_DOMAIN;
6 | var VERSION = CURRENT_VERSION;
7 |
8 | main(TARGET_DOMAIN, SCRIPT_DOMAIN, VERSION);
9 |
--------------------------------------------------------------------------------
/src/scripts/uploaddata_latest_local.js:
--------------------------------------------------------------------------------
1 | import main from "./uploaddata_core";
2 | import { CURRENT_VERSION } from "../constants";
3 |
4 | var TARGET_DOMAIN = "http://127.0.0.1:5000";
5 | var SCRIPT_DOMAIN = TARGET_DOMAIN;
6 | var VERSION = CURRENT_VERSION;
7 |
8 | main(TARGET_DOMAIN, SCRIPT_DOMAIN, VERSION);
9 |
--------------------------------------------------------------------------------
/src/scripts/uploaddata_matixx.js:
--------------------------------------------------------------------------------
1 | import main from "./uploaddata_core";
2 |
3 | var TARGET_DOMAIN = "http://gsv.fun";
4 | var SCRIPT_DOMAIN = "//gitadora-skill-viewer.herokuapp.com";
5 | var VERSION = "matixx";
6 |
7 | main(TARGET_DOMAIN, SCRIPT_DOMAIN, VERSION);
8 |
--------------------------------------------------------------------------------
/src/scripts/uploaddata_matixx_dev.js:
--------------------------------------------------------------------------------
1 | import main from "./uploaddata_core";
2 |
3 | var TARGET_DOMAIN = "//gitadora-skill-viewer-dev.herokuapp.com";
4 | var SCRIPT_DOMAIN = TARGET_DOMAIN;
5 | var VERSION = "matixx";
6 |
7 | main(TARGET_DOMAIN, SCRIPT_DOMAIN, VERSION);
8 |
--------------------------------------------------------------------------------
/src/scripts/uploaddata_matixx_local.js:
--------------------------------------------------------------------------------
1 | import main from "./uploaddata_core";
2 |
3 | var TARGET_DOMAIN = "http://127.0.0.1:5000";
4 | var SCRIPT_DOMAIN = TARGET_DOMAIN;
5 | var VERSION = "matixx";
6 |
7 | main(TARGET_DOMAIN, SCRIPT_DOMAIN, VERSION);
8 |
--------------------------------------------------------------------------------
/src/scripts/uploaddata_nextage.js:
--------------------------------------------------------------------------------
1 | import main from "./uploaddata_core";
2 |
3 | var TARGET_DOMAIN = "http://gsv.fun";
4 | var SCRIPT_DOMAIN = "//gitadora-skill-viewer.herokuapp.com";
5 | var VERSION = "nextage";
6 |
7 | main(TARGET_DOMAIN, SCRIPT_DOMAIN, VERSION);
8 |
--------------------------------------------------------------------------------
/src/scripts/uploaddata_nextage_dev.js:
--------------------------------------------------------------------------------
1 | import main from "./uploaddata_core";
2 |
3 | var TARGET_DOMAIN = "//gitadora-skill-viewer-dev.herokuapp.com";
4 | var SCRIPT_DOMAIN = TARGET_DOMAIN;
5 | var VERSION = "nextage";
6 |
7 | main(TARGET_DOMAIN, SCRIPT_DOMAIN, VERSION);
8 |
--------------------------------------------------------------------------------
/src/scripts/uploaddata_nextage_local.js:
--------------------------------------------------------------------------------
1 | import main from "./uploaddata_core";
2 |
3 | var TARGET_DOMAIN = "http://127.0.0.1:5000";
4 | var SCRIPT_DOMAIN = TARGET_DOMAIN;
5 | var VERSION = "nextage";
6 |
7 | main(TARGET_DOMAIN, SCRIPT_DOMAIN, VERSION);
8 |
--------------------------------------------------------------------------------
/src/scripts/uploaddata_tbre.js:
--------------------------------------------------------------------------------
1 | import main from "./uploaddata_core";
2 |
3 | var TARGET_DOMAIN = "http://gsv.fun";
4 | var SCRIPT_DOMAIN = "//gitadora-skill-viewer.herokuapp.com";
5 | var VERSION = "tbre";
6 |
7 | main(TARGET_DOMAIN, SCRIPT_DOMAIN, VERSION);
8 |
--------------------------------------------------------------------------------
/src/scripts/uploaddata_tbre_dev.js:
--------------------------------------------------------------------------------
1 | import main from "./uploaddata_core";
2 |
3 | var TARGET_DOMAIN = "//gitadora-skill-viewer-dev.herokuapp.com";
4 | var SCRIPT_DOMAIN = TARGET_DOMAIN;
5 | var VERSION = "tbre";
6 |
7 | main(TARGET_DOMAIN, SCRIPT_DOMAIN, VERSION);
8 |
--------------------------------------------------------------------------------
/src/scripts/uploaddata_tbre_local.js:
--------------------------------------------------------------------------------
1 | import main from "./uploaddata_core";
2 |
3 | var TARGET_DOMAIN = "http://127.0.0.1:3000";
4 | var SCRIPT_DOMAIN = TARGET_DOMAIN;
5 | var VERSION = "tbre";
6 |
7 | main(TARGET_DOMAIN, SCRIPT_DOMAIN, VERSION);
8 |
--------------------------------------------------------------------------------
/src/scripts/uploaddata_template.js:
--------------------------------------------------------------------------------
1 | import main from "./uploaddata_core";
2 |
3 | var TARGET_DOMAIN = process.env.TARGET_DOMAIN;
4 | var SCRIPT_DOMAIN = process.env.SCRIPT_DOMAIN;
5 | var VERSION = process.env.VERSION;
6 |
7 | main(TARGET_DOMAIN, SCRIPT_DOMAIN, VERSION);
8 |
--------------------------------------------------------------------------------
/src/server.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { renderToString } from "react-dom/server";
3 | import { StaticRouter } from "react-router";
4 | import { IntlProvider } from "react-intl";
5 | import flatten from "flat";
6 | import { Helmet } from "react-helmet";
7 | import { ServerStyleSheets as MuiServerStyleSheets } from "@material-ui/styles";
8 | import { ApolloProvider } from "@apollo/react-common";
9 | import { InMemoryCache } from "apollo-cache-inmemory";
10 | import { HttpLink } from "apollo-link-http";
11 | import { ApolloClient } from "apollo-client";
12 | import { getDataFromTree } from "@apollo/react-ssr";
13 | import fetch from "node-fetch";
14 | import { ServerStyleSheet } from "styled-components";
15 | import { DOMParser } from "xmldom";
16 |
17 | import jaMessages from "../locales/ja/common.json";
18 | import zhMessages from "../locales/zh/common.json";
19 | import enMessages from "../locales/en/common.json";
20 | import koMessages from "../locales/ko/common.json";
21 |
22 | import htmlTemplate from "./views/htmlTemplate";
23 | import App from "./react/App.jsx";
24 |
25 | // for polyfilling https://github.com/formatjs/react-intl/blob/master/docs/Getting-Started.md#domparser
26 | global.DOMParser = DOMParser;
27 |
28 | const bundleFileName = readBundleFileNameFromManifest();
29 |
30 | function readBundleFileNameFromManifest() {
31 | let bundleFileName;
32 | try {
33 | let stats = JSON.parse(
34 | require("fs")
35 | .readFileSync("./src/public/js/manifest.json")
36 | .toString()
37 | );
38 | bundleFileName = stats["bundle.js"];
39 | } catch (e) {
40 | console.error("Failes to load stats file", e);
41 | }
42 | return bundleFileName;
43 | }
44 |
45 | const messages = {
46 | ja: flatten(jaMessages),
47 | zh: flatten(zhMessages),
48 | en: flatten(enMessages),
49 | ko: flatten(koMessages)
50 | };
51 |
52 | const reactRoute = (req, res) => {
53 | if (req.get("Host") === "gitadora-skill-viewer.herokuapp.com" && process.env.DOMAIN_REDIRECT === "true") {
54 | res.redirect(301, `http://gsv.fun${req.url}`);
55 | } else {
56 | // set current language to cookie
57 | const locale = req.params.locale;
58 | res.cookie("locale", locale);
59 |
60 | // for mui components
61 | const client = new ApolloClient({
62 | ssrMode: true,
63 | cache: new InMemoryCache(),
64 | link: new HttpLink({
65 | uri: `${process.env.APP_URL}/graphql`,
66 | fetch
67 | })
68 | });
69 | const sheet = new ServerStyleSheet();
70 | const muiSheet = new MuiServerStyleSheets();
71 |
72 | const initialThemeKey = req.cookies.gsv_theme;
73 | let Temp = sheet.collectStyles(
74 | muiSheet.collect(
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 | )
83 | );
84 |
85 | getDataFromTree(Temp).then(() => {
86 | const renderedString = renderToString(Temp);
87 |
88 | // for styled component
89 | const styleTags = sheet.getStyleTags(); // or sheet.getStyleElement();
90 | // for mui
91 | const cssForMui = muiSheet.toString();
92 | // for i18n
93 | const appString = JSON.stringify({
94 | locale,
95 | messages: messages[locale],
96 | initialThemeKey
97 | });
98 | // for SEO
99 | const helmet = Helmet.renderStatic();
100 |
101 | const isDevelopment = process.env.NODE_ENV === "development";
102 |
103 | const bundlePath = isDevelopment ? "http://localhost:8000/js/bundle.js" : `/js/${bundleFileName}`;
104 |
105 | const html = htmlTemplate({
106 | googleSiteVerfication: process.env.GOOGLE_SITE_VERIFICATION,
107 | helmet,
108 | content: renderedString,
109 | appString,
110 | bundlePath,
111 | client,
112 | styleTags,
113 | cssForMui
114 | });
115 | res.send(html);
116 | });
117 | }
118 | };
119 |
120 | export default reactRoute;
121 |
--------------------------------------------------------------------------------
/src/theme.js:
--------------------------------------------------------------------------------
1 | const COLORS = {
2 | // for dark mode
3 | white: "white",
4 | lightGray_e3: "#e3e3e3",
5 | gray_bb: "#bbbbbb",
6 | darkGray_31: "#313131",
7 | black_1f: "#1f1f1f",
8 | black: "black",
9 |
10 | babyBlueEyes: "#a8c7fa",
11 | folly: "#f50057",
12 |
13 | // for default mode
14 | blue: "#0000EE",
15 | red: "red"
16 | };
17 |
18 | const defaultTheme = {
19 | key: "default",
20 | main: "",
21 | mainBg: COLORS.white,
22 | link: COLORS.blue,
23 | header: {
24 | title: COLORS.black_1f,
25 | bottomLine: COLORS.black,
26 | button: COLORS.black_1f,
27 | popover: "",
28 | popoverBg: COLORS.white,
29 | popoverHeader: "",
30 | popoverHoverBg: "rgba(0, 0, 0, 0.04)"
31 | },
32 | index: {
33 | subHeader: COLORS.white,
34 | subHeaderBg: COLORS.darkGray_31,
35 | imageDesc: COLORS.red,
36 | imageDescBg: COLORS.white,
37 | imageOpacity: 1,
38 | scriptBg: COLORS.lightGray_e3,
39 | alertOpacity: 1,
40 | snackBarBg: ""
41 | },
42 | skill: {
43 | profileTableHeader: COLORS.white,
44 | table: COLORS.black_87,
45 | tableHeaderBg: COLORS.darkGray_31,
46 | tableBg: COLORS.black,
47 | tableBgHexOpacity: "",
48 | saveButtonOpacity: 1,
49 | rivalIdInputBorder: "",
50 | rivalIdInputCaret: "",
51 | rivalIdInputPlaceholder: ""
52 | },
53 | list: {
54 | table: "",
55 | tableBg: "",
56 | tableContent: COLORS.white,
57 | tableContentBg: COLORS.black
58 | },
59 | kasegi: {
60 | table: COLORS.black,
61 | tableBg: "",
62 | tableBgHexOpacity: ""
63 | }
64 | };
65 |
66 | const darkTheme = {
67 | key: "dark",
68 | main: COLORS.gray_bb,
69 | mainBg: COLORS.black_1f,
70 | link: COLORS.babyBlueEyes,
71 | header: {
72 | title: COLORS.gray_bb,
73 | bottomLine: COLORS.gray_bb,
74 | button: COLORS.gray_bb,
75 | popover: COLORS.babyBlueEyes,
76 | popoverBg: COLORS.darkGray_31,
77 | popoverHeader: COLORS.gray_bb,
78 | popoverHoverBg: "rgba(255, 255, 255, 0.08)"
79 | },
80 | index: {
81 | subHeader: COLORS.black_1f,
82 | subHeaderBg: COLORS.gray_bb,
83 | imageDesc: COLORS.folly,
84 | imageDescBg: COLORS.black_1f,
85 | imageOpacity: 0.4,
86 | scriptBg: COLORS.darkGray_31,
87 | alertOpacity: 0.8,
88 | snackBarBg: COLORS.gray_bb
89 | },
90 | skill: {
91 | profileTableHeader: "",
92 | table: COLORS.black_1f,
93 | tableHeaderBg: COLORS.darkGray_31,
94 | tableBg: COLORS.black,
95 | tableBgHexOpacity: "CC",
96 | saveButtonOpacity: 0.8,
97 | rivalIdInputBorder: COLORS.white,
98 | rivalIdInputCaret: COLORS.white,
99 | rivalIdInputPlaceholder: COLORS.white
100 | },
101 | list: {
102 | table: COLORS.black_1f,
103 | tableBg: COLORS.lightGray_e3,
104 | tableContent: COLORS.gray_bb,
105 | tableContentBg: COLORS.black_1f
106 | },
107 | kasegi: {
108 | table: COLORS.black_1f,
109 | tableBg: COLORS.lightGray_e3,
110 | tableBgHexOpacity: "99"
111 | }
112 | };
113 |
114 | const theme = { default: defaultTheme, dark: darkTheme };
115 |
116 | export default theme;
117 |
--------------------------------------------------------------------------------
/src/views/htmlTemplate.js:
--------------------------------------------------------------------------------
1 | const htmlTemplate = ({
2 | helmet,
3 | content,
4 | appString,
5 | bundlePath,
6 | googleSiteVerfication,
7 | client,
8 | styleTags,
9 | cssForMui
10 | }) => {
11 | return `
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | ${helmet.title.toString()}
20 | ${helmet.meta.toString()}
21 | ${helmet.link.toString()}
22 | ${helmet.style.toString()}
23 |
24 |
25 |
26 |
27 | ${styleTags}
28 |
29 |
30 |
31 | ${content}
32 |
36 |
37 |
38 |
39 | `;
40 | };
41 |
42 | export default htmlTemplate;
43 |
--------------------------------------------------------------------------------
/webpack.client.config.js:
--------------------------------------------------------------------------------
1 | require("dotenv").config();
2 | const path = require("path");
3 | const CompressionPlugin = require("compression-webpack-plugin");
4 | const ManifestPlugin = require("webpack-manifest-plugin");
5 |
6 | const PUBLIC_JS_DIR = path.resolve(__dirname, "src/public/js");
7 | const SRC_DIR = path.resolve(__dirname, "src");
8 |
9 | const isDevelopment = process.env.NODE_ENV === "development";
10 |
11 | const clientConfig = {
12 | mode: isDevelopment ? "development" : "production",
13 | devServer: {
14 | contentBase: "./src/public",
15 | port: 8000
16 | },
17 | entry: {
18 | bundle: ["@babel/polyfill", `${SRC_DIR}/client.js`]
19 | },
20 | output: {
21 | path: PUBLIC_JS_DIR,
22 | filename: isDevelopment ? "[name].js" : "[name].[contenthash].js"
23 | },
24 | module: {
25 | rules: [
26 | {
27 | test: /\.jsx?$/,
28 | use: [
29 | {
30 | loader: "babel-loader"
31 | }
32 | ],
33 | include: SRC_DIR
34 | },
35 | {
36 | test: /\.scss$/,
37 | use: [
38 | {
39 | loader: "style-loader"
40 | },
41 | {
42 | loader: "css-loader?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]"
43 | },
44 | {
45 | loader: "sass-loader"
46 | }
47 | ]
48 | }
49 | ]
50 | },
51 | node: {
52 | fs: "empty"
53 | },
54 | plugins: [
55 | !isDevelopment &&
56 | new CompressionPlugin({
57 | asset: "[path].gz[query]",
58 | algorithm: "gzip",
59 | test: /\.js$|\.css$|\.html$/,
60 | threshold: 10240
61 | }),
62 | !isDevelopment && new ManifestPlugin()
63 | ].filter(Boolean)
64 | };
65 |
66 | module.exports = clientConfig;
67 |
--------------------------------------------------------------------------------
/webpack.scripts.config.js:
--------------------------------------------------------------------------------
1 | const webpack = require("webpack");
2 | const path = require("path");
3 | const { ON_SERVICE_VERSIONS, CURRENT_VERSION } = require("./src/constants");
4 |
5 | const DIST_DIR = path.resolve(__dirname, "src/public/js");
6 | const SRC_DIR = path.resolve(__dirname, "src");
7 |
8 | var createScriptsConfig = (version, flag) => ({
9 | mode: "production",
10 | entry: {
11 | uploaddata_latest: `${SRC_DIR}/scripts/uploaddata_template.js`
12 | },
13 | output: {
14 | path: DIST_DIR,
15 | filename: flag
16 | ? `uploaddata_${version}_${flag}.js`
17 | : `uploaddata_${version}.js`
18 | },
19 | module: {
20 | rules: [
21 | {
22 | test: /\.jsx?$/,
23 | use: [
24 | {
25 | loader: "babel-loader"
26 | }
27 | ],
28 | include: SRC_DIR
29 | }
30 | ]
31 | },
32 | plugins: [
33 | new webpack.DefinePlugin({
34 | "process.env.TARGET_DOMAIN":
35 | flag === "local"
36 | ? '"http://127.0.0.1:5000"'
37 | : flag === "dev"
38 | ? '"//gitadora-skill-viewer-dev.herokuapp.com"'
39 | : '"http://gsv.fun"',
40 | "process.env.SCRIPT_DOMAIN":
41 | flag === "local"
42 | ? '"http://127.0.0.1:5000"'
43 | : '"//gitadora-skill-viewer.herokuapp.com"',
44 | "process.env.VERSION":
45 | version === "latest" ? `"${CURRENT_VERSION}"` : `"${version}"`
46 | })
47 | ]
48 | });
49 |
50 | let moduleExports = [];
51 |
52 | moduleExports.push(createScriptsConfig("latest"));
53 | moduleExports.push(createScriptsConfig("latest", "dev"));
54 | moduleExports.push(createScriptsConfig("latest", "local"));
55 |
56 | ON_SERVICE_VERSIONS.slice(1).forEach(version => {
57 | moduleExports.push(createScriptsConfig(version));
58 | moduleExports.push(createScriptsConfig(version, "dev"));
59 | moduleExports.push(createScriptsConfig(version, "local"));
60 | });
61 |
62 | module.exports = moduleExports;
63 |
--------------------------------------------------------------------------------
/webpack.server.config.js:
--------------------------------------------------------------------------------
1 | require("dotenv").config();
2 | const path = require("path");
3 | const nodeExternals = require("webpack-node-externals");
4 |
5 | const SRC_DIR = path.resolve(__dirname, "src");
6 |
7 | const serverConfig = {
8 | target: "node", // use require() & use NodeJs CommonJS style
9 | externals: [nodeExternals()], // in order to ignore all modules in node_modules folder
10 | mode: "development",
11 | entry: {
12 | server: ["@babel/polyfill", `${__dirname}/app.js`]
13 | },
14 | output: {
15 | path: __dirname,
16 | filename: "app.dist.js"
17 | },
18 | module: {
19 | rules: [
20 | {
21 | test: /\.jsx?$/,
22 | use: [
23 | {
24 | loader: "babel-loader"
25 | }
26 | ],
27 | include: SRC_DIR
28 | },
29 | {
30 | test: /\.scss$/,
31 | use: [
32 | {
33 | loader: "style-loader"
34 | },
35 | {
36 | loader:
37 | "css-loader?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]"
38 | },
39 | {
40 | loader: "sass-loader"
41 | }
42 | ]
43 | }
44 | ]
45 | }
46 | };
47 |
48 | module.exports = serverConfig;
49 |
--------------------------------------------------------------------------------