From 19554ef4dd76454d0c37b38cfc7ec002adade598 Mon Sep 17 00:00:00 2001 From: RaenonX Date: Fri, 17 Sep 2021 15:43:40 -0500 Subject: [PATCH 01/19] FIX - Failing tests Signed-off-by: RaenonX --- .../gameData/unit/searcher/main.test.tsx | 49 ++++++++++++++++++- .../unitInfo/lookup/out/main.test.tsx | 13 +---- test/data/resources | 2 +- 3 files changed, 50 insertions(+), 14 deletions(-) diff --git a/src/components/elements/gameData/unit/searcher/main.test.tsx b/src/components/elements/gameData/unit/searcher/main.test.tsx index 3bbaa985..93298c98 100644 --- a/src/components/elements/gameData/unit/searcher/main.test.tsx +++ b/src/components/elements/gameData/unit/searcher/main.test.tsx @@ -32,6 +32,7 @@ describe('Unit searcher', () => { generateInputData={fnGenerateInputData} renderOutput={fnRenderOutput} renderCount={3} + getSortedUnitInfo={(unitInfo) => unitInfo} isUnitPrioritized={() => true} isLoading={false} /> @@ -50,6 +51,7 @@ describe('Unit searcher', () => { generateInputData={fnGenerateInputData} renderOutput={fnRenderOutput} renderCount={-1} + getSortedUnitInfo={(unitInfo) => unitInfo} isUnitPrioritized={() => true} isLoading={false} /> @@ -70,6 +72,7 @@ describe('Unit searcher', () => { generateInputData={fnGenerateInputData} renderOutput={fnRenderOutput} renderCount={3} + getSortedUnitInfo={(unitInfo) => unitInfo} isUnitPrioritized={() => true} isLoading={false} /> @@ -92,6 +95,7 @@ describe('Unit searcher', () => { generateInputData={fnGenerateInputData} renderOutput={fnRenderOutput} renderCount={3} + getSortedUnitInfo={(unitInfo) => unitInfo} isUnitPrioritized={() => true} isLoading={false} /> @@ -114,6 +118,7 @@ describe('Unit searcher', () => { generateInputData={fnGenerateInputData} renderOutput={fnRenderOutput} renderCount={3} + getSortedUnitInfo={(unitInfo) => unitInfo} isUnitPrioritized={() => true} isLoading={false} /> @@ -136,6 +141,7 @@ describe('Unit searcher', () => { generateInputData={fnGenerateInputData} renderOutput={fnRenderOutput} renderCount={3} + getSortedUnitInfo={(unitInfo) => unitInfo} isUnitPrioritized={() => true} isLoading={false} /> @@ -173,7 +179,8 @@ describe('Unit searcher', () => { generateInputData={fnGenerateInputData} renderOutput={fnRenderOutput} renderCount={3} - isUnitPrioritized={(unitInfo) => unitInfo.element === Element.SHADOW} + getSortedUnitInfo={(unitInfo) => unitInfo.sort((info) => info.id ? -1 : 1)} + isUnitPrioritized={(info) => info.element === Element.SHADOW} isLoading={false} /> )); @@ -184,4 +191,44 @@ describe('Unit searcher', () => { const prioritizedInfo = fnRenderOutput.mock.calls[0][0].prioritizedUnitInfo as Array; expect(prioritizedInfo.map((info) => info.element === Element.SHADOW)).toBeTruthy(); }); + + it('prioritizes unit info according to its desired order', async () => { + renderReact(() => ( + 'Unit ID'}} + generateInputData={fnGenerateInputData} + renderOutput={fnRenderOutput} + renderCount={3} + getSortedUnitInfo={(unitInfo) => unitInfo} + isUnitPrioritized={(info) => [10150106, 10150304, 10150305, 10150404].includes(info.id)} + isLoading={false} + /> + )); + + const searchButton = await screen.findByText(translationEN.misc.search, {selector: 'button:enabled'}); + userEvent.click(searchButton); + + const prioritizedUnitInfo = fnRenderOutput.mock.calls[0][0].prioritizedUnitInfo as Array; + expect(prioritizedUnitInfo.every((info) => [10150106, 10150304, 10150305].includes(info.id))).toBeTruthy(); + }); + + it('renders prioritized unit info first', async () => { + renderReact(() => ( + 'Unit ID'}} + generateInputData={fnGenerateInputData} + renderOutput={fnRenderOutput} + renderCount={5} + getSortedUnitInfo={(unitInfo) => unitInfo} + isUnitPrioritized={(info) => [10150106, 10350103, 10350405, 10950101].includes(info.id)} + isLoading={false} + /> + )); + + const searchButton = await screen.findByText(translationEN.misc.search, {selector: 'button:enabled'}); + userEvent.click(searchButton); + + const prioritizedUnitInfo = fnRenderOutput.mock.calls[0][0].prioritizedUnitInfo as Array; + expect(prioritizedUnitInfo.map((info) => info.id)).toHaveLength(4); + }); }); diff --git a/src/components/pages/gameData/unitInfo/lookup/out/main.test.tsx b/src/components/pages/gameData/unitInfo/lookup/out/main.test.tsx index b932ae6b..bc8c44a5 100644 --- a/src/components/pages/gameData/unitInfo/lookup/out/main.test.tsx +++ b/src/components/pages/gameData/unitInfo/lookup/out/main.test.tsx @@ -1,24 +1,15 @@ import React from 'react'; -import {waitFor} from '@testing-library/react'; - import unitInfo from '../../../../../../../test/data/resources/info/chara.json'; import {renderReact} from '../../../../../../../test/render/main'; import {overrideObject} from '../../../../../../utils/override'; -import {sortFunc} from '../in/sort/lookup'; import {InputData} from '../in/types'; import {generateInputData} from '../in/utils'; import {UnitInfoLookupOutput} from './main'; describe('Unit info lookup output', () => { - let fnSortByUnitId: jest.SpyInstance; - - beforeEach(() => { - fnSortByUnitId = jest.spyOn(sortFunc, 'unitId'); - }); - - it('sorts the output', async () => { + it('renders the output', async () => { const inputData: InputData = overrideObject(generateInputData(), {sortBy: 'unitId'}); renderReact(() => ( @@ -29,7 +20,5 @@ describe('Unit info lookup output', () => { analyses={{}} /> )); - - await waitFor(() => expect(fnSortByUnitId).toHaveBeenCalled()); }); }); diff --git a/test/data/resources b/test/data/resources index 69c60f0f..565a0b34 160000 --- a/test/data/resources +++ b/test/data/resources @@ -1 +1 @@ -Subproject commit 69c60f0fd74213676477ccb50248100e55f750a9 +Subproject commit 565a0b34b9ea5a491b4af2b5873864563c8dab4a From 1e6391d42f2b5212b03143756b12f2e940b4b149 Mon Sep 17 00:00:00 2001 From: RaenonX Date: Fri, 17 Sep 2021 15:44:59 -0500 Subject: [PATCH 02/19] FIX - Unit info sorting missing units #306 Signed-off-by: RaenonX --- .../elements/gameData/unit/filter/main.tsx | 2 +- .../elements/gameData/unit/searcher/main.tsx | 28 ++++++++++++------- .../gameData/unitInfo/lookup/main.test.tsx | 15 ++++------ .../pages/gameData/unitInfo/lookup/main.tsx | 8 +++++- .../gameData/unitInfo/lookup/out/main.tsx | 7 ++--- src/components/pages/tier/main.tsx | 8 +++++- src/components/pages/tier/out/main.tsx | 4 +-- 7 files changed, 41 insertions(+), 31 deletions(-) diff --git a/src/components/elements/gameData/unit/filter/main.tsx b/src/components/elements/gameData/unit/filter/main.tsx index c5c91201..32ef5ac9 100644 --- a/src/components/elements/gameData/unit/filter/main.tsx +++ b/src/components/elements/gameData/unit/filter/main.tsx @@ -106,7 +106,7 @@ export const UnitFilter = diff --git a/src/components/elements/gameData/unit/searcher/main.tsx b/src/components/elements/gameData/unit/searcher/main.tsx index f656743b..0157c4dd 100644 --- a/src/components/elements/gameData/unit/searcher/main.tsx +++ b/src/components/elements/gameData/unit/searcher/main.tsx @@ -21,6 +21,7 @@ type Props< E2 extends EnumEntry, V > = Pick, 'sortOrderNames' | 'generateInputData' | 'getAdditionalInputs'> & { + getSortedUnitInfo: (unitInfo: Array, inputData: D) => Array, renderIfAdmin?: React.ReactNode, renderOutput: (props: UnitSearchOutputProps) => React.ReactNode, renderCount: number, @@ -39,6 +40,7 @@ export const UnitSearcher = < sortOrderNames, generateInputData, getAdditionalInputs, + getSortedUnitInfo, renderIfAdmin, renderOutput, renderCount, @@ -55,18 +57,20 @@ export const UnitSearcher = < const [inputData, setInputData] = React.useState(); const [resultCount, setResultCount] = React.useState(renderCount); // < 0 = don't limit - // Get the filtered unit info and see if it needs to be sliced - let filteredUnitInfo = getFilteredUnitInfo(inputData, charaInfo, dragonInfo, nameRef) - .sort((unitInfo) => isUnitPrioritized(unitInfo) ? -1 : 1); + let unitInfoProcessed = getFilteredUnitInfo(inputData, charaInfo, dragonInfo, nameRef); - const isSliced = resultCount > 0 && filteredUnitInfo.length > resultCount; - if (isSliced) { - filteredUnitInfo = filteredUnitInfo.slice(0, resultCount); + let prioritizedUnitInfo = unitInfoProcessed.filter((info) => isUnitPrioritized(info)); + const otherUnitInfo = unitInfoProcessed.filter((info) => !isUnitPrioritized(info)); + if (inputData) { + prioritizedUnitInfo = getSortedUnitInfo(prioritizedUnitInfo, inputData); } - // Separate to prioritized and others - const prioritizedUnitInfo = filteredUnitInfo.filter((info) => isUnitPrioritized(info)); - const otherUnitInfo = filteredUnitInfo.filter((info) => !isUnitPrioritized(info)); + unitInfoProcessed = [...prioritizedUnitInfo, ...otherUnitInfo]; + + const isSliced = resultCount > 0 && unitInfoProcessed.length > resultCount; + if (isSliced) { + unitInfoProcessed = unitInfoProcessed.slice(0, resultCount); + } // Scroll after input data has changed React.useEffect(() => { @@ -92,7 +96,11 @@ export const UnitSearcher = < {context?.session?.user.isAdmin && renderIfAdmin}
- {inputData && renderOutput({inputData, prioritizedUnitInfo, otherUnitInfo})} + {inputData && renderOutput({ + inputData, + prioritizedUnitInfo: unitInfoProcessed.filter((info) => isUnitPrioritized(info)), + otherUnitInfo: unitInfoProcessed.filter((info) => !isUnitPrioritized(info)), + })}
{ isSliced && diff --git a/src/components/pages/gameData/unitInfo/lookup/main.test.tsx b/src/components/pages/gameData/unitInfo/lookup/main.test.tsx index b88efb54..78db9feb 100644 --- a/src/components/pages/gameData/unitInfo/lookup/main.test.tsx +++ b/src/components/pages/gameData/unitInfo/lookup/main.test.tsx @@ -115,31 +115,26 @@ describe('Analysis lookup page', () => { }); it('scrolls on not found, also scrolls on found', async () => { - fnGetLookup.mockResolvedValue(lookupResponseNoAnalyses); + fnGetLookup.mockResolvedValue(lookupResponseHasAnalyses); const {rerender} = renderReact(() => ); - expect(fnGetLookupLanding).toHaveBeenCalledTimes(1); - const keywordInput = screen.getByPlaceholderText(translationEN.misc.searchKeyword); const searchButton = await screen.findByText(translationEN.misc.search, {selector: 'button:enabled'}); userEvent.type(keywordInput, 'AAA'); userEvent.click(searchButton); - expect(fnGetLookup).toHaveBeenCalledTimes(1); await waitFor(() => expect(fnScroll).toHaveBeenCalledTimes(2)); expect(screen.queryByText('Gala Leonidas')).not.toBeInTheDocument(); - fnGetLookup.mockResolvedValue(lookupResponseHasAnalyses); rerender(); expect(fnGetLookupLanding).toHaveBeenCalledTimes(1); userEvent.clear(keywordInput); userEvent.click(searchButton); - expect(fnGetLookup).toHaveBeenCalledTimes(1); - await waitFor(() => expect(screen.getByAltText('Gala Leonidas')).toBeInTheDocument()); - expect(fnScroll).toHaveBeenCalledTimes(3); - }, 15000); // Finding `Gala Leonidas` is time-consuming, causing false negative + await waitFor(() => expect(fnScroll).toHaveBeenCalledTimes(3)); + expect(await screen.findByAltText('Gala Leonidas')).toBeInTheDocument(); + }); it('searches by element and type', async () => { fnGetLookup.mockResolvedValue(lookupResponseNoAnalyses); @@ -154,7 +149,7 @@ describe('Analysis lookup page', () => { userEvent.click(searchButton); expect(fnGetLookup).toHaveBeenCalledTimes(1); - await waitFor(() => expect(screen.getByAltText('Gala Leonidas')).toBeInTheDocument()); + await waitFor(() => expect(screen.getByAltText('Panther')).toBeInTheDocument()); expect(screen.queryByAltText('Karina')).not.toBeInTheDocument(); }); diff --git a/src/components/pages/gameData/unitInfo/lookup/main.tsx b/src/components/pages/gameData/unitInfo/lookup/main.tsx index dc475797..8a6b8bfb 100644 --- a/src/components/pages/gameData/unitInfo/lookup/main.tsx +++ b/src/components/pages/gameData/unitInfo/lookup/main.tsx @@ -10,7 +10,7 @@ import {useFetchState} from '../../../../elements/common/fetch'; import {UnitSearcher} from '../../../../elements/gameData/unit/searcher/main'; import {PostManageBar} from '../../../../elements/posts/manageBar'; import {UnitInfoLookupLanding} from './in/landing'; -import {orderName} from './in/sort/lookup'; +import {orderName, sortFunc} from './in/sort/lookup'; import {generateInputData} from './in/utils'; import {MaxEntriesToDisplay} from './out/const'; import {UnitInfoLookupOutput} from './out/main'; @@ -77,6 +77,12 @@ export const UnitInfoLookup = () => { renderCount={MaxEntriesToDisplay} onSearchRequested={(inputData) => GoogleAnalytics.analysisLookup(inputData)} isUnitPrioritized={(info) => info.id in analysisMeta.data.analyses} + getSortedUnitInfo={(unitInfo, inputData) => ( + unitInfo + .map((info) => ({unitInfo: info, lookupInfo: analysisMeta.data.analyses[info.id]})) + .sort(sortFunc[inputData.sortBy]) + .map((item) => item.unitInfo) + )} isLoading={analysisMeta.fetching} /> diff --git a/src/components/pages/gameData/unitInfo/lookup/out/main.tsx b/src/components/pages/gameData/unitInfo/lookup/out/main.tsx index a2fe94d9..a02c9066 100644 --- a/src/components/pages/gameData/unitInfo/lookup/out/main.tsx +++ b/src/components/pages/gameData/unitInfo/lookup/out/main.tsx @@ -6,7 +6,6 @@ import Form from 'react-bootstrap/Form'; import {UnitInfoLookupAnalyses} from '../../../../../../api-def/api'; import {useI18n} from '../../../../../../i18n/hook'; import {UnitSearchOutputProps} from '../../../../../elements/gameData/unit/searcher/types'; -import {sortFunc} from '../in/sort/lookup'; import {InputData, SortOrder} from '../in/types'; import {UnitInfoEntry} from './entry'; @@ -16,7 +15,6 @@ type AnalysisLookupOutputProps = UnitSearchOutputProps & { } export const UnitInfoLookupOutput = ({ - inputData, prioritizedUnitInfo, otherUnitInfo, analyses, @@ -25,13 +23,12 @@ export const UnitInfoLookupOutput = ({ // Split to prioritize the units that have analysis const unitInfoHasAnalysis = prioritizedUnitInfo - .map((info) => ({unitInfo: info, lookupInfo: analyses[info.id]})) - .sort(sortFunc[inputData.sortBy]); + .map((info) => ({unitInfo: info, lookupInfo: analyses[info.id]})); const unitInfoNoAnalysis = otherUnitInfo .map((info) => ({unitInfo: info, lookupInfo: undefined})); const unitInfoSorted = [...unitInfoHasAnalysis, ...unitInfoNoAnalysis]; - if (!prioritizedUnitInfo.length && !otherUnitInfo.length) { + if (!unitInfoSorted.length) { return (
{t((t) => t.posts.analysis.error.noResult)} diff --git a/src/components/pages/tier/main.tsx b/src/components/pages/tier/main.tsx index 7b732969..29c24234 100644 --- a/src/components/pages/tier/main.tsx +++ b/src/components/pages/tier/main.tsx @@ -9,7 +9,7 @@ import {ApiRequestSender} from '../../../utils/services/api/requestSender'; import {ButtonBar} from '../../elements/common/buttonBar'; import {useFetchStateProcessed} from '../../elements/common/fetch'; import {UnitSearcher} from '../../elements/gameData/unit/searcher/main'; -import {MaxEntriesToDisplay, orderName} from './const'; +import {MaxEntriesToDisplay, orderName, sortFunc} from './const'; import {useKeyPointData} from './hooks'; import {TierListOutput} from './out/main'; import {Display, DisplayOption, InputData} from './types'; @@ -71,6 +71,12 @@ export const TierList = () => { )} renderCount={MaxEntriesToDisplay} isUnitPrioritized={(info) => info.id in tierData.data} + getSortedUnitInfo={(unitInfo, inputData) => ( + unitInfo + .map((info) => ({unitInfo: info, tierNote: tierData.data[info.id]})) + .sort(sortFunc[inputData.sortBy]) + .map((obj) => obj.unitInfo) + )} isLoading={tierData.fetching} /> ); diff --git a/src/components/pages/tier/out/main.tsx b/src/components/pages/tier/out/main.tsx index 3f8fb4b4..f6c63f96 100644 --- a/src/components/pages/tier/out/main.tsx +++ b/src/components/pages/tier/out/main.tsx @@ -8,7 +8,6 @@ import {UnitTierData} from '../../../../api-def/api'; import {useI18n} from '../../../../i18n/hook'; import {AdsTierResultsEnd} from '../../../elements/common/ads/main'; import {UnitSearchOutputProps} from '../../../elements/gameData/unit/searcher/types'; -import {sortFunc} from '../const'; import {IconCompDependent} from '../icons'; import {InputData, PropsUseKeyPointData, SortOrder} from '../types'; import {TierListOutputShowAll} from './all/main'; @@ -23,8 +22,7 @@ export const TierListOutput = ({inputData, tierData, prioritizedUnitInfo, otherU const {t} = useI18n(); const entryPackHasTierNote = prioritizedUnitInfo - .map((info) => ({unitInfo: info, tierNote: tierData[info.id]})) - .sort(sortFunc[inputData.sortBy]); + .map((info) => ({unitInfo: info, tierNote: tierData[info.id]})); const entryPackNoTierNote = otherUnitInfo .map((info) => ({unitInfo: info, tierNote: undefined})); From 3f3c06139a915dfc25d44137695140f807bb5c4c Mon Sep 17 00:00:00 2001 From: RaenonX Date: Fri, 17 Sep 2021 15:45:39 -0500 Subject: [PATCH 03/19] OTH - Bump app version Signed-off-by: RaenonX --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 56e30a1b..cf2ac1d1 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "dragalia-site-front", "description": "Frontend of Dragalia Lost info website by OM.", "repository": "https://github.com/RaenonX-DL/dragalia-site-front", - "version": "2.14.2", + "version": "2.15.0", "engines": { "node": "14.x", "npm": "^7.12.0" From c00d030d5c835987dddfa7981b53c9f504977aa4 Mon Sep 17 00:00:00 2001 From: RaenonX Date: Tue, 21 Sep 2021 22:37:24 -0500 Subject: [PATCH 04/19] IMP - Updated API definitions and env vars for story data Signed-off-by: RaenonX --- .azure/variables/main.yml | 3 +++ README.md | 10 ++++++++-- src/api-def | 2 +- test/data/resources | 2 +- 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/.azure/variables/main.yml b/.azure/variables/main.yml index 5986106d..f83896b6 100644 --- a/.azure/variables/main.yml +++ b/.azure/variables/main.yml @@ -9,6 +9,9 @@ variables: - name: NEXT_PUBLIC_DEPOT_ROOT value: https://raw.githubusercontent.com/RaenonX-DL/dragalia-data-depot/main readonly: true + - name: NEXT_PUBLIC_AUDIO_ROOT + value: https://raw.githubusercontent.com/RaenonX-DL/dragalia-data-audio/main + readonly: true - name: NEXT_PUBLIC_GA_ID value: G-796E69CFJG readonly: true diff --git a/README.md b/README.md index 8c9dcc9f..39921c95 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,7 @@ Name | Required/Optional | Description `NEXT_PUBLIC_API_ROOT` | Required | Root URL of the backend. This should **not** end with a slash (`/`). `NEXT_PUBLIC_RESOURCE_ROOT` | Required | Root URL of the exported resources. This should **not** end with a slash (`/`). `NEXT_PUBLIC_DEPOT_ROOT` | Required | Root URL of the data depot. This should **not** end with a slash (`/`). +`NEXT_PUBLIC_AUDIO_ROOT` | Required | Root URL of the audio depot. This should **not** end with a slash (`/`). For the [current deployed website][front-site], `NEXT_PUBLIC_API_ROOT` is `https://dl-back.raenonx.cc`. @@ -53,12 +54,17 @@ In general, `NEXT_PUBLIC_RESOURCE_ROOT` is `https://raw.githubusercontent.com/RaenonX-DL/dragalia-site-resources/main`, where stores the parsed data. -- Check https://github.com/RaenonX-DL/dragalia-site-resources for all available resources. +> Check https://github.com/RaenonX-DL/dragalia-site-resources for all available resources. `NEXT_PUBLIC_DEPOT_ROOT` is `https://raw.githubusercontent.com/RaenonX-DL/dragalia-data-depot/main`, where stores the dumped game assets. -- Check https://github.com/RaenonX-DL/dragalia-data-depot for all available resources. +> Check https://github.com/RaenonX-DL/dragalia-data-depot for all available resources. + +`NEXT_PUBLIC_AUDIO_ROOT` is `https://raw.githubusercontent.com/RaenonX-DL/dragalia-data-audio/main`, +where stores the dumped audio files. + +> Check https://github.com/RaenonX-DL/dragalia-data-audio for all available resources. [front-repo]: https://github.com/RaenonX-DL/dragalia-site-front [front-site]: https://dl.raenonx.cc diff --git a/src/api-def b/src/api-def index b639bb25..7baecc20 160000 --- a/src/api-def +++ b/src/api-def @@ -1 +1 @@ -Subproject commit b639bb257eade27eddbb828a9ba7b1a831230737 +Subproject commit 7baecc207f7d1ff744bda3fd03065573b56e0212 diff --git a/test/data/resources b/test/data/resources index 565a0b34..52d1b184 160000 --- a/test/data/resources +++ b/test/data/resources @@ -1 +1 @@ -Subproject commit 565a0b34b9ea5a491b4af2b5873864563c8dab4a +Subproject commit 52d1b1847d4c06e62ce77fc86a2d305798d59ab5 From b53e1e8bc5ff92d41c7b8b82d9fcae7f10e4453c Mon Sep 17 00:00:00 2001 From: RaenonX Date: Tue, 21 Sep 2021 23:11:15 -0500 Subject: [PATCH 05/19] ADD - Skeleton for unit story page #4 Signed-off-by: RaenonX --- package-lock.json | 69 ++++++++++++++++---- package.json | 2 +- pages/[lang]/story.ts | 4 -- pages/[lang]/story/event/[id].ts | 4 ++ pages/[lang]/story/main/[id].ts | 4 ++ pages/[lang]/story/unit/[id].ts | 4 ++ src/components/pages/gameData/story/main.tsx | 6 ++ src/const/path/definitions.ts | 11 +++- src/i18n/translations/cht/translation.ts | 6 ++ src/i18n/translations/definition.ts | 3 + src/i18n/translations/en/translation.ts | 6 ++ src/i18n/translations/jp/translation.ts | 8 ++- src/utils/meta/main.ts | 2 + src/utils/meta/preprocess.ts | 13 ++-- src/utils/meta/translations.ts | 5 +- 15 files changed, 117 insertions(+), 30 deletions(-) delete mode 100644 pages/[lang]/story.ts create mode 100644 pages/[lang]/story/event/[id].ts create mode 100644 pages/[lang]/story/main/[id].ts create mode 100644 pages/[lang]/story/unit/[id].ts create mode 100644 src/components/pages/gameData/story/main.tsx diff --git a/package-lock.json b/package-lock.json index fd676cee..8643e13a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { "name": "dragalia-site-front", - "version": "2.14.0", + "version": "2.15.0", "lockfileVersion": 2, "requires": true, "packages": { "": { - "version": "2.14.0", + "version": "2.15.0", "dependencies": { "@ctrl/react-adsense": "^1.3.1", "@reduxjs/toolkit": "^1.6.1", @@ -27,8 +27,7 @@ "react-timeago": "^6.2.1", "redux-persist": "^6.0.0", "remark-gfm": "^1.0.0", - "universal-cookie": "^4.0.4", - "uuid": "^8.3.2" + "universal-cookie": "^4.0.4" }, "devDependencies": { "@testing-library/jest-dom": "^5.14.1", @@ -54,7 +53,7 @@ "eslint-config-next": "^11.1.2", "eslint-plugin-import": "^2.24.2", "eslint-plugin-jest-dom": "^3.9.0", - "eslint-plugin-react": "^7.25.1", + "eslint-plugin-react": "^7.26.0", "eslint-plugin-react-hooks": "^4.2.0", "eslint-plugin-testing-library": "^4.12.2", "eslint-plugin-unused-imports": "^1.1.4", @@ -7419,23 +7418,24 @@ } }, "node_modules/eslint-plugin-react": { - "version": "7.25.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.25.1.tgz", - "integrity": "sha512-P4j9K1dHoFXxDNP05AtixcJEvIT6ht8FhYKsrkY0MPCPaUMYijhpWwNiRDZVtA8KFuZOkGSeft6QwH8KuVpJug==", + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.26.0.tgz", + "integrity": "sha512-dceliS5itjk4EZdQYtLMz6GulcsasguIs+VTXuiC7Q5IPIdGTkyfXVdmsQOqEhlD9MciofH4cMcT1bw1WWNxCQ==", "dev": true, "dependencies": { "array-includes": "^3.1.3", "array.prototype.flatmap": "^1.2.4", "doctrine": "^2.1.0", "estraverse": "^5.2.0", - "has": "^1.0.3", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.0.4", "object.entries": "^1.1.4", "object.fromentries": "^2.0.4", + "object.hasown": "^1.0.0", "object.values": "^1.1.4", "prop-types": "^15.7.2", "resolve": "^2.0.0-next.3", + "semver": "^6.3.0", "string.prototype.matchall": "^4.0.5" }, "engines": { @@ -7482,6 +7482,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/eslint-plugin-react/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/eslint-plugin-testing-library": { "version": "4.12.2", "resolved": "https://registry.npmjs.org/eslint-plugin-testing-library/-/eslint-plugin-testing-library-4.12.2.tgz", @@ -13585,6 +13594,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object.hasown": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.0.0.tgz", + "integrity": "sha512-qYMF2CLIjxxLGleeM0jrcB4kiv3loGVAjKQKvH8pSU/i2VcRRvUNmxbD+nEMmrXRfORhuVJuH8OtSYCZoue3zA==", + "dev": true, + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.18.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/object.values": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.4.tgz", @@ -19109,6 +19131,7 @@ "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, "bin": { "uuid": "dist/bin/uuid" } @@ -25417,23 +25440,24 @@ } }, "eslint-plugin-react": { - "version": "7.25.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.25.1.tgz", - "integrity": "sha512-P4j9K1dHoFXxDNP05AtixcJEvIT6ht8FhYKsrkY0MPCPaUMYijhpWwNiRDZVtA8KFuZOkGSeft6QwH8KuVpJug==", + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.26.0.tgz", + "integrity": "sha512-dceliS5itjk4EZdQYtLMz6GulcsasguIs+VTXuiC7Q5IPIdGTkyfXVdmsQOqEhlD9MciofH4cMcT1bw1WWNxCQ==", "dev": true, "requires": { "array-includes": "^3.1.3", "array.prototype.flatmap": "^1.2.4", "doctrine": "^2.1.0", "estraverse": "^5.2.0", - "has": "^1.0.3", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.0.4", "object.entries": "^1.1.4", "object.fromentries": "^2.0.4", + "object.hasown": "^1.0.0", "object.values": "^1.1.4", "prop-types": "^15.7.2", "resolve": "^2.0.0-next.3", + "semver": "^6.3.0", "string.prototype.matchall": "^4.0.5" }, "dependencies": { @@ -25455,6 +25479,12 @@ "is-core-module": "^2.2.0", "path-parse": "^1.0.6" } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true } } }, @@ -30053,6 +30083,16 @@ "has": "^1.0.3" } }, + "object.hasown": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.0.0.tgz", + "integrity": "sha512-qYMF2CLIjxxLGleeM0jrcB4kiv3loGVAjKQKvH8pSU/i2VcRRvUNmxbD+nEMmrXRfORhuVJuH8OtSYCZoue3zA==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.18.1" + } + }, "object.values": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.4.tgz", @@ -34319,7 +34359,8 @@ "uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true }, "v8-compile-cache": { "version": "2.3.0", diff --git a/package.json b/package.json index cf2ac1d1..813418d4 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "eslint-config-next": "^11.1.2", "eslint-plugin-import": "^2.24.2", "eslint-plugin-jest-dom": "^3.9.0", - "eslint-plugin-react": "^7.25.1", + "eslint-plugin-react": "^7.26.0", "eslint-plugin-react-hooks": "^4.2.0", "eslint-plugin-testing-library": "^4.12.2", "eslint-plugin-unused-imports": "^1.1.4", diff --git a/pages/[lang]/story.ts b/pages/[lang]/story.ts deleted file mode 100644 index 01b9ed1d..00000000 --- a/pages/[lang]/story.ts +++ /dev/null @@ -1,4 +0,0 @@ -import {Constructing} from '../../src/components/pages/constructing'; - - -export default Constructing; diff --git a/pages/[lang]/story/event/[id].ts b/pages/[lang]/story/event/[id].ts new file mode 100644 index 00000000..98753a32 --- /dev/null +++ b/pages/[lang]/story/event/[id].ts @@ -0,0 +1,4 @@ +import {Constructing} from '../../../../src/components/pages/constructing'; + + +export default Constructing; diff --git a/pages/[lang]/story/main/[id].ts b/pages/[lang]/story/main/[id].ts new file mode 100644 index 00000000..98753a32 --- /dev/null +++ b/pages/[lang]/story/main/[id].ts @@ -0,0 +1,4 @@ +import {Constructing} from '../../../../src/components/pages/constructing'; + + +export default Constructing; diff --git a/pages/[lang]/story/unit/[id].ts b/pages/[lang]/story/unit/[id].ts new file mode 100644 index 00000000..08906f0f --- /dev/null +++ b/pages/[lang]/story/unit/[id].ts @@ -0,0 +1,4 @@ +import {UnitStory} from '../../../../src/components/pages/gameData/story/main'; + + +export default UnitStory; diff --git a/src/components/pages/gameData/story/main.tsx b/src/components/pages/gameData/story/main.tsx new file mode 100644 index 00000000..8931471f --- /dev/null +++ b/src/components/pages/gameData/story/main.tsx @@ -0,0 +1,6 @@ +import React from 'react'; + + +export const UnitStory = () => { + return <>WIP; +}; diff --git a/src/const/path/definitions.ts b/src/const/path/definitions.ts index 185daebb..a28d3efc 100644 --- a/src/const/path/definitions.ts +++ b/src/const/path/definitions.ts @@ -5,6 +5,13 @@ export enum DataPath { TIER_KEY_POINT = '/tier/points/[id]', } +// Must and only have `id` as the key for story ID +export enum StoryPath { + STORY_UNIT = '/story/unit/[id]', + STORY_MAIN = '/story/main/[id]', + STORY_EVENT = '/story/event/[id]', +} + // Must and only have `id` as the key for unit ID export enum UnitPath { UNIT_INFO = '/info/[id]', @@ -55,9 +62,9 @@ export enum AuthPath { } export const allPaths = ([] as Array).concat( - ...[DataPath, UnitPath, PostPath, GeneralPath, AuthPath].map( + ...[DataPath, UnitPath, PostPath, StoryPath, GeneralPath, AuthPath].map( (paths) => Object.values(paths), ), ); -export type PagePath = DataPath | UnitPath | PostPath | GeneralPath | AuthPath; +export type PagePath = DataPath | UnitPath | PostPath | StoryPath | GeneralPath | AuthPath; diff --git a/src/i18n/translations/cht/translation.ts b/src/i18n/translations/cht/translation.ts index ee7698a8..0a5e02b6 100644 --- a/src/i18n/translations/cht/translation.ts +++ b/src/i18n/translations/cht/translation.ts @@ -682,6 +682,12 @@ export const translation: TranslationStruct = { description: '設定物件名稱的頁面。', }, }, + story: { + unit: { + title: '【角色故事】{{unitName}}', + description: '{{unitName}} 的角色故事全集。', + }, + }, }, error: { 401: { diff --git a/src/i18n/translations/definition.ts b/src/i18n/translations/definition.ts index 6efcfa39..d03efc0a 100644 --- a/src/i18n/translations/definition.ts +++ b/src/i18n/translations/definition.ts @@ -542,6 +542,9 @@ export type TranslationStruct = { info: PageMetaTranslations, name: PageMetaTranslations, }, + story: { + unit: PageMetaTranslations, + } }, error: { 401: PageMetaTranslations, diff --git a/src/i18n/translations/en/translation.ts b/src/i18n/translations/en/translation.ts index 39538cdc..edd9d8a1 100644 --- a/src/i18n/translations/en/translation.ts +++ b/src/i18n/translations/en/translation.ts @@ -729,6 +729,12 @@ export const translation: TranslationStruct = { description: 'Page to configure the custom unit names.', }, }, + story: { + unit: { + title: '【Unit Story】{{unitName}}', + description: 'All unit stories of {{unitName}}.', + }, + }, }, error: { 401: { diff --git a/src/i18n/translations/jp/translation.ts b/src/i18n/translations/jp/translation.ts index f68cb084..19802b7a 100644 --- a/src/i18n/translations/jp/translation.ts +++ b/src/i18n/translations/jp/translation.ts @@ -680,13 +680,19 @@ export const translation: TranslationStruct = { unit: { info: { title: '{{unitName}}', - description: ' {{unitName}}に関する情報', + description: ' {{unitName}}に関する情報。', }, name: { title: 'ユニット名前設定ページ', description: 'ユニット名前を設定する。', }, }, + story: { + unit: { + title: '【ユニットストーリー】{{unitName}}', + description: '全部の{{unitName}}のストーリー。', + }, + }, }, error: { 401: { diff --git a/src/utils/meta/main.ts b/src/utils/meta/main.ts index 0ec81a58..6024796e 100644 --- a/src/utils/meta/main.ts +++ b/src/utils/meta/main.ts @@ -21,6 +21,8 @@ export const getPageMeta = async (context: AppContext): Promise => { const metaTFunc = getMetaTFunction(translations[lang]); const metaTFuncOnNotFound: GetTranslationFunction = (t) => t.meta.error['404']; + console.log('PATHNAMENOLANG', pathnameNoLang); + // Early return if `pathname` is not a valid page path - consider as 404 if (!isPagePath(pathnameNoLang)) { return { diff --git a/src/utils/meta/preprocess.ts b/src/utils/meta/preprocess.ts index 3d1db744..546745f2 100644 --- a/src/utils/meta/preprocess.ts +++ b/src/utils/meta/preprocess.ts @@ -2,6 +2,7 @@ import {getSession} from 'next-auth/client'; import {AppContext} from 'next/app'; import {FailedResponse, PageMetaResponse, SupportedLanguages} from '../../api-def/api'; +import {StoryPath} from '../../const/path/definitions'; import {isDataPath, isPostPath, isUnitPath} from '../../const/path/utils'; import {ApiRequestSender} from '../services/api/requestSender'; import {pathPostType} from './lookup'; @@ -18,10 +19,8 @@ export const getPageMetaPromise = async ({ }: PageMetaPromiseArgs): Promise => { const uid = (await getSession(context.ctx))?.user?.id.toString() || ''; - let responsePromise = ApiRequestSender.getPageMeta(uid, lang); - if (isDataPath(pathnameNoLang)) { - responsePromise = ApiRequestSender.getDataMeta( + return ApiRequestSender.getDataMeta( uid, lang, 'tierKeyPoint', @@ -30,7 +29,7 @@ export const getPageMetaPromise = async ({ } if (isPostPath(pathnameNoLang)) { - responsePromise = ApiRequestSender.getPostMeta( + return ApiRequestSender.getPostMeta( uid, lang, pathPostType[pathnameNoLang], @@ -38,13 +37,13 @@ export const getPageMetaPromise = async ({ ); } - if (isUnitPath(pathnameNoLang)) { - responsePromise = ApiRequestSender.getUnitMeta( + if (isUnitPath(pathnameNoLang) || pathnameNoLang === StoryPath.STORY_UNIT) { + return ApiRequestSender.getUnitMeta( uid, lang, context.router.query.id as string, ); } - return responsePromise; + return ApiRequestSender.getPageMeta(uid, lang); }; diff --git a/src/utils/meta/translations.ts b/src/utils/meta/translations.ts index 626d6dd1..db2a47ca 100644 --- a/src/utils/meta/translations.ts +++ b/src/utils/meta/translations.ts @@ -1,4 +1,4 @@ -import {AuthPath, DataPath, GeneralPath, PagePath, PostPath, UnitPath} from '../../const/path/definitions'; +import {AuthPath, DataPath, GeneralPath, PagePath, PostPath, StoryPath, UnitPath} from '../../const/path/definitions'; import {PageMetaTranslations} from '../../i18n/translations/definition'; import {GetTranslationFunction} from '../../i18n/types'; @@ -14,6 +14,7 @@ export const metaTransFunctions: { [path in PagePath]: GetTranslationFunction t.meta.inUse.post.analysis.edit, [PostPath.MISC]: (t) => t.meta.inUse.post.misc.post, [PostPath.MISC_EDIT]: (t) => t.meta.inUse.post.misc.edit, + [StoryPath.STORY_UNIT]: (t) => t.meta.inUse.story.unit, [GeneralPath.HOME]: (t) => t.meta.inUse.home, [GeneralPath.QUEST_LIST]: (t) => t.meta.inUse.post.quest.list, [GeneralPath.QUEST_NEW]: (t) => t.meta.inUse.post.quest.new, @@ -35,6 +36,8 @@ export const metaTransFunctions: { [path in PagePath]: GetTranslationFunction t.meta.temp.constructing, [GeneralPath.STORY]: (t) => t.meta.temp.constructing, [GeneralPath.ROTATION_CALC]: (t) => t.meta.temp.constructing, + [StoryPath.STORY_MAIN]: (t) => t.meta.inUse.post.misc.edit, + [StoryPath.STORY_EVENT]: (t) => t.meta.inUse.post.misc.edit, // Legacy [GeneralPath.ANALYSIS_LIST]: (t) => t.meta.inUse.gameData.info, }; From eb4e9dfaa6c8be669e2e8020c24e04fe8b6c3034 Mon Sep 17 00:00:00 2001 From: RaenonX Date: Tue, 21 Sep 2021 23:46:33 -0500 Subject: [PATCH 06/19] IMP - Removed unused `console.log` Signed-off-by: RaenonX --- src/utils/meta/main.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/utils/meta/main.ts b/src/utils/meta/main.ts index 6024796e..0ec81a58 100644 --- a/src/utils/meta/main.ts +++ b/src/utils/meta/main.ts @@ -21,8 +21,6 @@ export const getPageMeta = async (context: AppContext): Promise => { const metaTFunc = getMetaTFunction(translations[lang]); const metaTFuncOnNotFound: GetTranslationFunction = (t) => t.meta.error['404']; - console.log('PATHNAMENOLANG', pathnameNoLang); - // Early return if `pathname` is not a valid page path - consider as 404 if (!isPagePath(pathnameNoLang)) { return { From 0684b330c964a91f2e86617d8b329a29da91b43e Mon Sep 17 00:00:00 2001 From: RaenonX Date: Wed, 22 Sep 2021 01:38:03 -0500 Subject: [PATCH 07/19] ADD - Unit story page (basic render) #4 Signed-off-by: RaenonX --- src/api-def | 2 +- .../pages/gameData/story/chapter.tsx | 25 +++ .../gameData/story/conversation/break.tsx | 6 + .../gameData/story/conversation/talk.tsx | 52 ++++++ .../gameData/story/conversation/types.ts | 6 + .../pages/gameData/story/main.module.css | 17 ++ .../pages/gameData/story/main.module.css.map | 1 + .../pages/gameData/story/main.module.scss | 22 +++ .../pages/gameData/story/main.test.tsx | 157 ++++++++++++++++++ src/components/pages/gameData/story/main.tsx | 40 ++++- src/utils/services/resources/loader.ts | 22 ++- test/data/resources | 2 +- 12 files changed, 348 insertions(+), 4 deletions(-) create mode 100644 src/components/pages/gameData/story/chapter.tsx create mode 100644 src/components/pages/gameData/story/conversation/break.tsx create mode 100644 src/components/pages/gameData/story/conversation/talk.tsx create mode 100644 src/components/pages/gameData/story/conversation/types.ts create mode 100644 src/components/pages/gameData/story/main.module.css create mode 100644 src/components/pages/gameData/story/main.module.css.map create mode 100644 src/components/pages/gameData/story/main.module.scss create mode 100644 src/components/pages/gameData/story/main.test.tsx diff --git a/src/api-def b/src/api-def index 7baecc20..88336471 160000 --- a/src/api-def +++ b/src/api-def @@ -1 +1 @@ -Subproject commit 7baecc207f7d1ff744bda3fd03065573b56e0212 +Subproject commit 88336471e75fe1e07e29b8dfaee8f56d2ade7a87 diff --git a/src/components/pages/gameData/story/chapter.tsx b/src/components/pages/gameData/story/chapter.tsx new file mode 100644 index 00000000..feee6988 --- /dev/null +++ b/src/components/pages/gameData/story/chapter.tsx @@ -0,0 +1,25 @@ +import React from 'react'; + +import {Story} from '../../../../api-def/resources'; +import {StoryBreak} from './conversation/break'; +import {StoryTalk} from './conversation/talk'; + + +type Props = { + chapter: Story, +} + +export const StoryChapter = ({chapter}: Props) => { + return ( + <> + {chapter.conversations.map((conversation, idx) => { + if (conversation.type === 'conversation') { + return ; + } + if (conversation.type === 'break') { + return ; + } + })} + + ); +}; diff --git a/src/components/pages/gameData/story/conversation/break.tsx b/src/components/pages/gameData/story/conversation/break.tsx new file mode 100644 index 00000000..6dc9ec0d --- /dev/null +++ b/src/components/pages/gameData/story/conversation/break.tsx @@ -0,0 +1,6 @@ +import React from 'react'; + + +export const StoryBreak = () => { + return
; +}; diff --git a/src/components/pages/gameData/story/conversation/talk.tsx b/src/components/pages/gameData/story/conversation/talk.tsx new file mode 100644 index 00000000..f75e6e79 --- /dev/null +++ b/src/components/pages/gameData/story/conversation/talk.tsx @@ -0,0 +1,52 @@ +import React from 'react'; + +import Col from 'react-bootstrap/Col'; +import Row from 'react-bootstrap/Row'; + +import {DepotPaths, StoryTalk as StoryTalkData} from '../../../../../api-def/resources'; +import {Image} from '../../../../elements/common/image'; +import styles from '../main.module.css'; +import {StoryConversationProps} from './types'; + + +type Props = StoryConversationProps + +export const StoryTalk = ({conversation}: Props) => { + if (conversation.isSys) { + return ( + + + {conversation.content} + + + ); + } + + return ( + <> + + + + {conversation.speakerName} + + + + + + { + conversation.speakerIcon ? + : + <> + } + + + {conversation.content} + + + + ); +}; diff --git a/src/components/pages/gameData/story/conversation/types.ts b/src/components/pages/gameData/story/conversation/types.ts new file mode 100644 index 00000000..af26a67d --- /dev/null +++ b/src/components/pages/gameData/story/conversation/types.ts @@ -0,0 +1,6 @@ +import {StoryConversation} from '../../../../../api-def/resources'; + + +export type StoryConversationProps = { + conversation: T +} diff --git a/src/components/pages/gameData/story/main.module.css b/src/components/pages/gameData/story/main.module.css new file mode 100644 index 00000000..a8ebd52a --- /dev/null +++ b/src/components/pages/gameData/story/main.module.css @@ -0,0 +1,17 @@ +div.sysMessage { + border: #505050 1px solid; + border-radius: 0.25rem; + text-align: center; + padding: 0.5rem; +} +div.speakerIcon { + height: 3rem; + width: 3rem; + margin-right: 0.5rem; +} + +img.speakerIcon { + border-radius: 0.25rem; +} + +/*# sourceMappingURL=main.module.css.map */ diff --git a/src/components/pages/gameData/story/main.module.css.map b/src/components/pages/gameData/story/main.module.css.map new file mode 100644 index 00000000..6d76fd47 --- /dev/null +++ b/src/components/pages/gameData/story/main.module.css.map @@ -0,0 +1 @@ +{"version":3,"sourceRoot":"","sources":["main.module.scss"],"names":[],"mappings":"AAGE;EACE;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;;;AAKF;EACE","file":"main.module.css"} \ No newline at end of file diff --git a/src/components/pages/gameData/story/main.module.scss b/src/components/pages/gameData/story/main.module.scss new file mode 100644 index 00000000..f83b69e3 --- /dev/null +++ b/src/components/pages/gameData/story/main.module.scss @@ -0,0 +1,22 @@ +@use "../../../../../styles/colors"; + +div { + &.sysMessage { + border: colors.$color-black-80 1px solid; + border-radius: 0.25rem; + text-align: center; + padding: 0.5rem; + } + + &.speakerIcon { + height: 3rem; + width: 3rem; + margin-right: 0.5rem; + } +} + +img { + &.speakerIcon { + border-radius: 0.25rem; + } +} diff --git a/src/components/pages/gameData/story/main.test.tsx b/src/components/pages/gameData/story/main.test.tsx new file mode 100644 index 00000000..2174ef8d --- /dev/null +++ b/src/components/pages/gameData/story/main.test.tsx @@ -0,0 +1,157 @@ +import React from 'react'; + +import {screen} from '@testing-library/react'; + +import {renderReact} from '../../../../../test/render/main'; +import {DepotPaths, StoryBook} from '../../../../api-def/resources'; +import {ResourceLoader} from '../../../../utils/services/resources/loader'; +import {UnitStory} from './main'; + + +describe('Unit story page', () => { + let fnGetStoryBook: jest.SpyInstance>; + + beforeEach(() => { + fnGetStoryBook = jest.spyOn(ResourceLoader, 'getStoryBook'); + }); + + it('shows
for thematic break', async () => { + fnGetStoryBook.mockResolvedValueOnce([ + { + id: 1, + title: 'story', + conversations: [{type: 'break'}], + }, + ]); + + renderReact( + () => , + {contextParams: {unitId: 10650503}}, + ); + + expect(await screen.findByText('', {selector: 'hr'})); + }); + + it('shows story content with speaker icon if available', async () => { + fnGetStoryBook.mockResolvedValueOnce([ + { + id: 1, + title: 'story', + conversations: [{ + type: 'conversation', + speakerName: 'speaker', + speakerIcon: 'icon.png', + content: 'content', + isSys: false, + audioPaths: [], + }], + }, + ]); + + renderReact( + () => , + {contextParams: {unitId: 10650503}}, + ); + + const icon = await screen.findByText('', {selector: 'img'}); + expect(icon).toHaveAttribute('src', DepotPaths.getStorySpeakerIconURL('icon.png')); + }); + + it('shows story content without speaker icon if not available', async () => { + fnGetStoryBook.mockResolvedValueOnce([ + { + id: 1, + title: 'story', + conversations: [{ + type: 'conversation', + speakerName: 'speaker', + speakerIcon: null, + content: 'content', + isSys: false, + audioPaths: [], + }], + }, + ]); + + renderReact( + () => , + {contextParams: {unitId: 10650503}}, + ); + + expect(await screen.findByText('speaker')).toBeInTheDocument(); + expect(screen.queryByText('', {selector: 'img'})).not.toBeInTheDocument(); + }); + + it('shows speaker name explicitly', async () => { + fnGetStoryBook.mockResolvedValueOnce([ + { + id: 1, + title: 'story', + conversations: [{ + type: 'conversation', + speakerName: 'speaker', + speakerIcon: null, + content: 'content', + isSys: false, + audioPaths: [], + }], + }, + ]); + + renderReact( + () => , + {contextParams: {unitId: 10650503}}, + ); + + expect(await screen.findByText('speaker')).toBeInTheDocument(); + }); + + it('shows story content', async () => { + fnGetStoryBook.mockResolvedValueOnce([ + { + id: 1, + title: 'story', + conversations: [{ + type: 'conversation', + speakerName: 'SYS', + speakerIcon: null, + content: 'content', + isSys: true, + audioPaths: [], + }], + }, + ]); + + renderReact( + () => , + {contextParams: {unitId: 10650503}}, + ); + + expect(await screen.findByText('content')).toBeInTheDocument(); + }); + + it('shows system message without speaker name', async () => { + fnGetStoryBook.mockResolvedValueOnce([ + { + id: 1, + title: 'story', + conversations: [{ + type: 'conversation', + speakerName: 'SYS', + speakerIcon: null, + content: 'content', + isSys: true, + audioPaths: [], + }], + }, + ]); + + renderReact( + () => , + {contextParams: {unitId: 10650503}}, + ); + + expect(await screen.findByText('content')).toBeInTheDocument(); + expect(screen.queryByText('SYS')).not.toBeInTheDocument(); + }); +}); diff --git a/src/components/pages/gameData/story/main.tsx b/src/components/pages/gameData/story/main.tsx index 8931471f..50c8e80c 100644 --- a/src/components/pages/gameData/story/main.tsx +++ b/src/components/pages/gameData/story/main.tsx @@ -1,6 +1,44 @@ import React from 'react'; +import {StoryBook} from '../../../../api-def/resources'; +import {useI18n} from '../../../../i18n/hook'; +import {ResourceLoader} from '../../../../utils/services/resources/loader'; +import {sortAscending} from '../../../../utils/sort'; +import {isNotFetched, useFetchState} from '../../../elements/common/fetch'; +import {Loading} from '../../../elements/common/loading'; +import {useUnitId} from '../../../elements/gameData/hook'; +import {CollapsibleSectionedContent} from '../../../elements/posts/output/section'; +import {StoryChapter} from './chapter'; + export const UnitStory = () => { - return <>WIP; + const {lang} = useI18n(); + const unitId = useUnitId(); + + if (!unitId) { + return <>; + } + + const { + fetchStatus: storyBook, + fetchFunction: fetchStoryBook, + } = useFetchState( + undefined, + () => ResourceLoader.getStoryBook(lang, unitId), + `Failed to fetch the unit story of ${unitId}`, + ); + + fetchStoryBook(); + + if (isNotFetched(storyBook) || !storyBook.data) { + return ; + } + + return ( + chapter.id}))} + getTitle={(chapter) => chapter.title} + renderSection={(chapter) => } + /> + ); }; diff --git a/src/utils/services/resources/loader.ts b/src/utils/services/resources/loader.ts index 829ce68e..262659dd 100644 --- a/src/utils/services/resources/loader.ts +++ b/src/utils/services/resources/loader.ts @@ -1,3 +1,4 @@ +import {SupportedLanguages} from '../../../api-def/api/other/lang'; import { AttackingSkillData, BuffParamEnums, @@ -11,10 +12,12 @@ import { ExBuffParams, InfoDataAdvanced, NormalAttackChain, - ResourcePaths, SimpleUnitInfo, + ResourcePaths, + SimpleUnitInfo, SkillEnums, SkillIdentifierInfo, StatusEnums, + StoryBook, WeaponTypeEnums, } from '../../../api-def/resources'; @@ -229,6 +232,23 @@ export class ResourceLoader { // endregion + // region Story + /** + * Get the story book of a unit. + * + * @function + * @param {SupportedLanguages} lang language of the stories + * @param {number} unitId unit ID to get the stories + * @param {function?} callback function to be called after fetching the resource + * @return {Promise} promise after the callback + */ + static getStoryBook( + lang: SupportedLanguages, unitId: number, callback?: (advancedInfo: StoryBook) => void, + ): Promise { + return ResourceLoader.fetchResources(ResourcePaths.getStoryDataURL(lang, unitId), callback); + } + // endregion + // region Misc /** * Get the element bonus data. diff --git a/test/data/resources b/test/data/resources index 52d1b184..f493ca43 160000 --- a/test/data/resources +++ b/test/data/resources @@ -1 +1 @@ -Subproject commit 52d1b1847d4c06e62ce77fc86a2d305798d59ab5 +Subproject commit f493ca43df3496c95b18b651abc9fd9c476a2e2e From a12daae10b8c4285d9fd2b4a257f4777413136cb Mon Sep 17 00:00:00 2001 From: RaenonX Date: Wed, 22 Sep 2021 01:50:26 -0500 Subject: [PATCH 08/19] ADD - Unit story page (ads) #4 Signed-off-by: RaenonX --- src/components/elements/common/ads/main.tsx | 7 +++++++ src/components/pages/gameData/story/main.tsx | 8 +++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/components/elements/common/ads/main.tsx b/src/components/elements/common/ads/main.tsx index b08625df..71edb956 100644 --- a/src/components/elements/common/ads/main.tsx +++ b/src/components/elements/common/ads/main.tsx @@ -62,3 +62,10 @@ export const AdsUnitKeyPointInfo = () => ( testId="ads-unit-key-point-info" /> ); + +export const AdsStory = () => ( + +); diff --git a/src/components/pages/gameData/story/main.tsx b/src/components/pages/gameData/story/main.tsx index 50c8e80c..f6cc41ca 100644 --- a/src/components/pages/gameData/story/main.tsx +++ b/src/components/pages/gameData/story/main.tsx @@ -4,6 +4,7 @@ import {StoryBook} from '../../../../api-def/resources'; import {useI18n} from '../../../../i18n/hook'; import {ResourceLoader} from '../../../../utils/services/resources/loader'; import {sortAscending} from '../../../../utils/sort'; +import {AdsStory} from '../../../elements/common/ads/main'; import {isNotFetched, useFetchState} from '../../../elements/common/fetch'; import {Loading} from '../../../elements/common/loading'; import {useUnitId} from '../../../elements/gameData/hook'; @@ -38,7 +39,12 @@ export const UnitStory = () => { chapter.id}))} getTitle={(chapter) => chapter.title} - renderSection={(chapter) => } + renderSection={(chapter) => ( + <> + + + + )} /> ); }; From 787b3c3abbee8171664d9fec2479706f1affce63 Mon Sep 17 00:00:00 2001 From: RaenonX Date: Wed, 22 Sep 2021 02:11:20 -0500 Subject: [PATCH 09/19] ADD - Unit story page (related links) #4 Signed-off-by: RaenonX --- .../pages/gameData/story/main.module.css | 9 +++ .../pages/gameData/story/main.module.css.map | 2 +- .../pages/gameData/story/main.module.scss | 12 +++ .../pages/gameData/story/main.test.tsx | 75 +++++++++++++++++++ src/components/pages/gameData/story/main.tsx | 34 ++++++--- src/components/pages/gameData/story/other.tsx | 52 +++++++++++++ 6 files changed, 173 insertions(+), 11 deletions(-) create mode 100644 src/components/pages/gameData/story/other.tsx diff --git a/src/components/pages/gameData/story/main.module.css b/src/components/pages/gameData/story/main.module.css index a8ebd52a..7004878a 100644 --- a/src/components/pages/gameData/story/main.module.css +++ b/src/components/pages/gameData/story/main.module.css @@ -9,9 +9,18 @@ div.speakerIcon { width: 3rem; margin-right: 0.5rem; } +div.mainImage { + text-align: center; +} +div.relatedLinks { + text-align: center; +} img.speakerIcon { border-radius: 0.25rem; } +img.mainImage { + max-height: 60vh; +} /*# sourceMappingURL=main.module.css.map */ diff --git a/src/components/pages/gameData/story/main.module.css.map b/src/components/pages/gameData/story/main.module.css.map index 6d76fd47..fa31d293 100644 --- a/src/components/pages/gameData/story/main.module.css.map +++ b/src/components/pages/gameData/story/main.module.css.map @@ -1 +1 @@ -{"version":3,"sourceRoot":"","sources":["main.module.scss"],"names":[],"mappings":"AAGE;EACE;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;;;AAKF;EACE","file":"main.module.css"} \ No newline at end of file +{"version":3,"sourceRoot":"","sources":["main.module.scss"],"names":[],"mappings":"AAGE;EACE;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;;AAGF;EACE;;AAGF;EACE;;;AAKF;EACE;;AAGF;EACE","file":"main.module.css"} \ No newline at end of file diff --git a/src/components/pages/gameData/story/main.module.scss b/src/components/pages/gameData/story/main.module.scss index f83b69e3..bdec1c25 100644 --- a/src/components/pages/gameData/story/main.module.scss +++ b/src/components/pages/gameData/story/main.module.scss @@ -13,10 +13,22 @@ div { width: 3rem; margin-right: 0.5rem; } + + &.mainImage { + text-align: center; + } + + &.relatedLinks { + text-align: center; + } } img { &.speakerIcon { border-radius: 0.25rem; } + + &.mainImage { + max-height: 60vh; + } } diff --git a/src/components/pages/gameData/story/main.test.tsx b/src/components/pages/gameData/story/main.test.tsx index 2174ef8d..fdc8a208 100644 --- a/src/components/pages/gameData/story/main.test.tsx +++ b/src/components/pages/gameData/story/main.test.tsx @@ -154,4 +154,79 @@ describe('Unit story page', () => { expect(await screen.findByText('content')).toBeInTheDocument(); expect(screen.queryByText('SYS')).not.toBeInTheDocument(); }); + + it('shows ads for normal users', async () => { + fnGetStoryBook.mockResolvedValueOnce([ + { + id: 1, + title: 'story', + conversations: [{ + type: 'conversation', + speakerName: 'SYS', + speakerIcon: null, + content: 'content', + isSys: true, + audioPaths: [], + }], + }, + ]); + + renderReact( + () => , + {contextParams: {unitId: 10650503}}, + ); + + expect(await screen.findByText('content')).toBeInTheDocument(); + expect(screen.getByTestId('ads-story')).toBeInTheDocument(); + }); + + it('hides ads for ads-free users', async () => { + fnGetStoryBook.mockResolvedValueOnce([ + { + id: 1, + title: 'story', + conversations: [{ + type: 'conversation', + speakerName: 'SYS', + speakerIcon: null, + content: 'content', + isSys: true, + audioPaths: [], + }], + }, + ]); + + renderReact( + () => , + {contextParams: {unitId: 10650503}, user: {adsFreeExpiry: new Date()}}, + ); + + expect(await screen.findByText('content')).toBeInTheDocument(); + expect(screen.queryByTestId('ads-story')).not.toBeInTheDocument(); + }); + + it('shows related links', async () => { + fnGetStoryBook.mockResolvedValueOnce([ + { + id: 107504041, + title: 'story', + conversations: [{ + type: 'conversation', + speakerName: 'SYS', + speakerIcon: null, + content: 'content', + isSys: true, + audioPaths: [], + }], + }, + ]); + + renderReact( + () => , + {contextParams: {unitId: 10750404}}, + ); + + expect(await screen.findByText('Analysis')).toBeInTheDocument(); + expect(screen.getByText('Info')).toBeInTheDocument(); + }); }); diff --git a/src/components/pages/gameData/story/main.tsx b/src/components/pages/gameData/story/main.tsx index f6cc41ca..c3ae69ec 100644 --- a/src/components/pages/gameData/story/main.tsx +++ b/src/components/pages/gameData/story/main.tsx @@ -1,8 +1,12 @@ import React from 'react'; +import Col from 'react-bootstrap/Col'; +import Row from 'react-bootstrap/Row'; + import {StoryBook} from '../../../../api-def/resources'; import {useI18n} from '../../../../i18n/hook'; import {ResourceLoader} from '../../../../utils/services/resources/loader'; +import {useUnitInfo} from '../../../../utils/services/resources/unitInfo/hooks'; import {sortAscending} from '../../../../utils/sort'; import {AdsStory} from '../../../elements/common/ads/main'; import {isNotFetched, useFetchState} from '../../../elements/common/fetch'; @@ -10,6 +14,7 @@ import {Loading} from '../../../elements/common/loading'; import {useUnitId} from '../../../elements/gameData/hook'; import {CollapsibleSectionedContent} from '../../../elements/posts/output/section'; import {StoryChapter} from './chapter'; +import {StoryOtherInfo} from './other'; export const UnitStory = () => { @@ -28,6 +33,8 @@ export const UnitStory = () => { () => ResourceLoader.getStoryBook(lang, unitId), `Failed to fetch the unit story of ${unitId}`, ); + const {unitInfoMap} = useUnitInfo(); + const unitInfo = unitInfoMap.get(unitId); fetchStoryBook(); @@ -36,15 +43,22 @@ export const UnitStory = () => { } return ( - chapter.id}))} - getTitle={(chapter) => chapter.title} - renderSection={(chapter) => ( - <> - - - - )} - /> + <> + {unitInfo && } + + + chapter.id}))} + getTitle={(chapter) => chapter.title} + renderSection={(chapter) => ( + <> + + + + )} + /> + + + ); }; diff --git a/src/components/pages/gameData/story/other.tsx b/src/components/pages/gameData/story/other.tsx new file mode 100644 index 00000000..fefa1739 --- /dev/null +++ b/src/components/pages/gameData/story/other.tsx @@ -0,0 +1,52 @@ +import React from 'react'; + +import Col from 'react-bootstrap/Col'; +import Row from 'react-bootstrap/Row'; + +import {DepotPaths, UnitInfoData} from '../../../../api-def/resources'; +import {PostPath, UnitPath} from '../../../../const/path/definitions'; +import {useI18n} from '../../../../i18n/hook'; +import {makePostUrl, makeUnitUrl} from '../../../../utils/path/make'; +import {Image} from '../../../elements/common/image'; +import {InternalLink} from '../../../elements/common/link/internal'; +import styles from './main.module.css'; + + +type Props = { + unitInfo: UnitInfoData, +} + +export const StoryOtherInfo = ({unitInfo}: Props) => { + const {t, lang} = useI18n(); + + return ( + <> + + + + + + + + t.game.unitInfo.links.analysis)} + /> + + + t.game.unitInfo.links.info)} + /> + + +
+ + ); +}; From 7b55373e892dd7be4bfbddde00602d773a46b3ef Mon Sep 17 00:00:00 2001 From: RaenonX Date: Wed, 22 Sep 2021 02:51:10 -0500 Subject: [PATCH 10/19] ADD - Link to story page for `` #311 Signed-off-by: RaenonX --- src/api-def | 2 +- src/components/elements/gameData/unit/link.test.tsx | 10 +++++++--- src/components/elements/gameData/unit/link.tsx | 12 ++++++++++-- src/const/path/definitions.ts | 6 +++--- src/i18n/translations/cht/translation.ts | 1 + src/i18n/translations/definition.ts | 1 + src/i18n/translations/en/translation.ts | 1 + src/i18n/translations/jp/translation.ts | 1 + src/utils/meta/preprocess.ts | 2 +- src/utils/meta/translations.ts | 6 +++--- src/utils/path/make.ts | 11 ++++++++++- test/data/resources | 2 +- 12 files changed, 40 insertions(+), 15 deletions(-) diff --git a/src/api-def b/src/api-def index 88336471..01ccf675 160000 --- a/src/api-def +++ b/src/api-def @@ -1 +1 @@ -Subproject commit 88336471e75fe1e07e29b8dfaee8f56d2ade7a87 +Subproject commit 01ccf6757977aba1f839bcc60c09ca246ee4ac29 diff --git a/src/components/elements/gameData/unit/link.test.tsx b/src/components/elements/gameData/unit/link.test.tsx index 6ba573fe..3e1c935e 100644 --- a/src/components/elements/gameData/unit/link.test.tsx +++ b/src/components/elements/gameData/unit/link.test.tsx @@ -6,8 +6,8 @@ import userEvent from '@testing-library/user-event'; import {renderReact} from '../../../../../test/render/main'; import {SupportedLanguages, UnitType} from '../../../../api-def/api'; import {DepotPaths} from '../../../../api-def/resources'; -import {PostPath, UnitPath} from '../../../../const/path/definitions'; -import {makePostUrl, makeUnitUrl} from '../../../../utils/path/make'; +import {PostPath, StoryPath, UnitPath} from '../../../../const/path/definitions'; +import {makePostUrl, makeStoryUrl, makeUnitUrl} from '../../../../utils/path/make'; import {UnitLink} from './link'; @@ -20,7 +20,7 @@ describe('Unit link', () => { expect(screen.queryByText('Info')).not.toBeInTheDocument(); }); - it('shows modal with the links to the analysis and the unit info page', async () => { + it('shows modal with correct related links', async () => { renderReact(() => ); const linkElement = await screen.findByText('Gala Leonidas', {selector: 'a'}); @@ -33,6 +33,10 @@ describe('Unit link', () => { const infoLink = screen.getByText('Info'); const expectedInfoLink = makeUnitUrl(UnitPath.UNIT_INFO, {id: 10950101, lang: SupportedLanguages.EN}); expect(infoLink).toHaveAttribute('href', expectedInfoLink); + + const storyLink = screen.getByText('Story'); + const expectedStoryLink = makeStoryUrl(StoryPath.UNIT, {id: 10950101, lang: SupportedLanguages.EN}); + expect(storyLink).toHaveAttribute('href', expectedStoryLink); }); it('shows modal without analysis if not exists', async () => { diff --git a/src/components/elements/gameData/unit/link.tsx b/src/components/elements/gameData/unit/link.tsx index e73676d8..82e7be39 100644 --- a/src/components/elements/gameData/unit/link.tsx +++ b/src/components/elements/gameData/unit/link.tsx @@ -4,9 +4,9 @@ import Button from 'react-bootstrap/Button'; import {UnitType} from '../../../../api-def/api'; import {DepotPaths} from '../../../../api-def/resources'; -import {PostPath, UnitPath} from '../../../../const/path/definitions'; +import {PostPath, StoryPath, UnitPath} from '../../../../const/path/definitions'; import {useI18n} from '../../../../i18n/hook'; -import {makePostUrl, makeUnitUrl} from '../../../../utils/path/make'; +import {makePostUrl, makeStoryUrl, makeUnitUrl} from '../../../../utils/path/make'; import {Image} from '../../common/image'; import {InternalLink} from '../../common/link/internal'; import {Loading} from '../../common/loading'; @@ -60,6 +60,14 @@ const ModalContent = ({unit, hasAnalysis, modalState, setModalState}: ModalConte content={t((t) => t.game.unitInfo.links.info)} /> + ); }; diff --git a/src/const/path/definitions.ts b/src/const/path/definitions.ts index a28d3efc..63cc4fd8 100644 --- a/src/const/path/definitions.ts +++ b/src/const/path/definitions.ts @@ -7,9 +7,9 @@ export enum DataPath { // Must and only have `id` as the key for story ID export enum StoryPath { - STORY_UNIT = '/story/unit/[id]', - STORY_MAIN = '/story/main/[id]', - STORY_EVENT = '/story/event/[id]', + UNIT = '/story/unit/[id]', + MAIN = '/story/main/[id]', + EVENT = '/story/event/[id]', } // Must and only have `id` as the key for unit ID diff --git a/src/i18n/translations/cht/translation.ts b/src/i18n/translations/cht/translation.ts index 0a5e02b6..c9fa2dd2 100644 --- a/src/i18n/translations/cht/translation.ts +++ b/src/i18n/translations/cht/translation.ts @@ -396,6 +396,7 @@ export const translation: TranslationStruct = { links: { analysis: '評測', info: '資訊', + story: '故事', }, text: { total: '(總計)', diff --git a/src/i18n/translations/definition.ts b/src/i18n/translations/definition.ts index d03efc0a..e2a005e7 100644 --- a/src/i18n/translations/definition.ts +++ b/src/i18n/translations/definition.ts @@ -391,6 +391,7 @@ export type TranslationStruct = { links: { analysis: string, info: string, + story: string, }, text: { total: string, diff --git a/src/i18n/translations/en/translation.ts b/src/i18n/translations/en/translation.ts index edd9d8a1..53a86a42 100644 --- a/src/i18n/translations/en/translation.ts +++ b/src/i18n/translations/en/translation.ts @@ -428,6 +428,7 @@ export const translation: TranslationStruct = { links: { analysis: 'Analysis', info: 'Info', + story: 'Story', }, text: { total: '(Total)', diff --git a/src/i18n/translations/jp/translation.ts b/src/i18n/translations/jp/translation.ts index 19802b7a..86df9a74 100644 --- a/src/i18n/translations/jp/translation.ts +++ b/src/i18n/translations/jp/translation.ts @@ -403,6 +403,7 @@ export const translation: TranslationStruct = { links: { analysis: '評価', info: 'キャラ情報', + story: 'ストーリー', }, text: { total: '(総計)', diff --git a/src/utils/meta/preprocess.ts b/src/utils/meta/preprocess.ts index 546745f2..e4ae6131 100644 --- a/src/utils/meta/preprocess.ts +++ b/src/utils/meta/preprocess.ts @@ -37,7 +37,7 @@ export const getPageMetaPromise = async ({ ); } - if (isUnitPath(pathnameNoLang) || pathnameNoLang === StoryPath.STORY_UNIT) { + if (isUnitPath(pathnameNoLang) || pathnameNoLang === StoryPath.UNIT) { return ApiRequestSender.getUnitMeta( uid, lang, diff --git a/src/utils/meta/translations.ts b/src/utils/meta/translations.ts index db2a47ca..f19522e2 100644 --- a/src/utils/meta/translations.ts +++ b/src/utils/meta/translations.ts @@ -14,7 +14,7 @@ export const metaTransFunctions: { [path in PagePath]: GetTranslationFunction t.meta.inUse.post.analysis.edit, [PostPath.MISC]: (t) => t.meta.inUse.post.misc.post, [PostPath.MISC_EDIT]: (t) => t.meta.inUse.post.misc.edit, - [StoryPath.STORY_UNIT]: (t) => t.meta.inUse.story.unit, + [StoryPath.UNIT]: (t) => t.meta.inUse.story.unit, [GeneralPath.HOME]: (t) => t.meta.inUse.home, [GeneralPath.QUEST_LIST]: (t) => t.meta.inUse.post.quest.list, [GeneralPath.QUEST_NEW]: (t) => t.meta.inUse.post.quest.new, @@ -36,8 +36,8 @@ export const metaTransFunctions: { [path in PagePath]: GetTranslationFunction t.meta.temp.constructing, [GeneralPath.STORY]: (t) => t.meta.temp.constructing, [GeneralPath.ROTATION_CALC]: (t) => t.meta.temp.constructing, - [StoryPath.STORY_MAIN]: (t) => t.meta.inUse.post.misc.edit, - [StoryPath.STORY_EVENT]: (t) => t.meta.inUse.post.misc.edit, + [StoryPath.MAIN]: (t) => t.meta.inUse.post.misc.edit, + [StoryPath.EVENT]: (t) => t.meta.inUse.post.misc.edit, // Legacy [GeneralPath.ANALYSIS_LIST]: (t) => t.meta.inUse.gameData.info, }; diff --git a/src/utils/path/make.ts b/src/utils/path/make.ts index f6dfb2d7..168b6ea5 100644 --- a/src/utils/path/make.ts +++ b/src/utils/path/make.ts @@ -1,5 +1,5 @@ import {SupportedLanguages} from '../../api-def/api'; -import {DataPath, GeneralPath, PostPath, UnitPath} from '../../const/path/definitions'; +import {DataPath, GeneralPath, PostPath, StoryPath, UnitPath} from '../../const/path/definitions'; const generateUrl = (path: string, args: { [key in string]: string | number }) => { @@ -46,3 +46,12 @@ type UnitPathArgs = PathArgs & { export const makeUnitUrl = (path: UnitPath, args: UnitPathArgs) => { return generateUrl(`/${args.lang}${path}`, args); }; + +// Needs to match the key names used in `StoryPath` +type StoryPathArgs = PathArgs & { + id: number, +} + +export const makeStoryUrl = (path: StoryPath, args: StoryPathArgs) => { + return generateUrl(`/${args.lang}${path}`, args); +}; diff --git a/test/data/resources b/test/data/resources index f493ca43..0c718351 160000 --- a/test/data/resources +++ b/test/data/resources @@ -1 +1 @@ -Subproject commit f493ca43df3496c95b18b651abc9fd9c476a2e2e +Subproject commit 0c7183519bbb4fd4554a468197e18898ad5b6735 From 407135fcef45b76506afc14d082ac11ccd4e738a Mon Sep 17 00:00:00 2001 From: RaenonX Date: Wed, 22 Sep 2021 04:56:28 -0500 Subject: [PATCH 11/19] ADD - Story voice auto-play #296 Signed-off-by: RaenonX --- package-lock.json | 21 +++ package.json | 1 + src/components/elements/common/icons.tsx | 6 + .../pages/gameData/story/audioControl.tsx | 58 +++++++ .../pages/gameData/story/audioHook.test.ts | 162 ++++++++++++++++++ .../pages/gameData/story/audioHook.ts | 78 +++++++++ .../pages/gameData/story/chapter.tsx | 22 ++- .../gameData/story/conversation/talk.tsx | 36 +++- .../gameData/story/conversation/types.ts | 6 - .../pages/gameData/story/main.module.css | 36 ++++ .../pages/gameData/story/main.module.css.map | 2 +- .../pages/gameData/story/main.module.scss | 49 ++++++ src/components/pages/gameData/story/types.ts | 19 ++ 13 files changed, 481 insertions(+), 15 deletions(-) create mode 100644 src/components/pages/gameData/story/audioControl.tsx create mode 100644 src/components/pages/gameData/story/audioHook.test.ts create mode 100644 src/components/pages/gameData/story/audioHook.ts delete mode 100644 src/components/pages/gameData/story/conversation/types.ts create mode 100644 src/components/pages/gameData/story/types.ts diff --git a/package-lock.json b/package-lock.json index 8643e13a..9ff56998 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "node-fetch": "^2.6.2", "pm2": "^5.1.1", "react": "^17.0.2", + "react-audio-player": "^0.17.0", "react-bootstrap": "^1.6.3", "react-dom": "^17.0.2", "react-markdown": "^6.0.3", @@ -15823,6 +15824,18 @@ "node": ">=10" } }, + "node_modules/react-audio-player": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-audio-player/-/react-audio-player-0.17.0.tgz", + "integrity": "sha512-aCZgusPxA9HK7rLZcTdhTbBH9l6do9vn3NorgoDZRxRxJlOy9uZWzPaKjd7QdcuP2vXpxGA/61JMnnOEY7NXeA==", + "dependencies": { + "prop-types": "^15.7.2" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, "node_modules/react-bootstrap": { "version": "1.6.3", "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-1.6.3.tgz", @@ -31846,6 +31859,14 @@ "whatwg-fetch": "^3.4.1" } }, + "react-audio-player": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-audio-player/-/react-audio-player-0.17.0.tgz", + "integrity": "sha512-aCZgusPxA9HK7rLZcTdhTbBH9l6do9vn3NorgoDZRxRxJlOy9uZWzPaKjd7QdcuP2vXpxGA/61JMnnOEY7NXeA==", + "requires": { + "prop-types": "^15.7.2" + } + }, "react-bootstrap": { "version": "1.6.3", "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-1.6.3.tgz", diff --git a/package.json b/package.json index 813418d4..69d4b169 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "node-fetch": "^2.6.2", "pm2": "^5.1.1", "react": "^17.0.2", + "react-audio-player": "^0.17.0", "react-bootstrap": "^1.6.3", "react-dom": "^17.0.2", "react-markdown": "^6.0.3", diff --git a/src/components/elements/common/icons.tsx b/src/components/elements/common/icons.tsx index bbc0ea6d..92ffb164 100644 --- a/src/components/elements/common/icons.tsx +++ b/src/components/elements/common/icons.tsx @@ -28,3 +28,9 @@ export const IconClipboard = () => ; export const IconRadar = () => ; export const IconNotes = () => ; + +export const IconPlay = () => ; + +export const IconPause = () => ; + +export const IconStop = () => ; diff --git a/src/components/pages/gameData/story/audioControl.tsx b/src/components/pages/gameData/story/audioControl.tsx new file mode 100644 index 00000000..da1a5847 --- /dev/null +++ b/src/components/pages/gameData/story/audioControl.tsx @@ -0,0 +1,58 @@ +import React from 'react'; + +import Button from 'react-bootstrap/Button'; +import ButtonGroup from 'react-bootstrap/ButtonGroup'; +import Col from 'react-bootstrap/Col'; +import Row from 'react-bootstrap/Row'; + +import {IconPause, IconPlay, IconStop} from '../../../elements/common/icons'; +import styles from './main.module.css'; +import {UseAudioControlReturn} from './types'; + + +type Props = { + hookReturn: UseAudioControlReturn, +} + +export const AudioControl = ({hookReturn}: Props) => { + const {playingState, startAudio, resumeAudio, pauseAudio, stopAudio} = hookReturn; + + const PlayButton = ({isStart = false}) => ( + + ); + + const PauseButton = () => ( + + ); + + const StopButton = () => ( + + ); + + if (playingState !== 'stopping') { + return ( + + + + {playingState === 'playing' ? : } + + + + + ); + } + + return ( + + + + + + ); +}; diff --git a/src/components/pages/gameData/story/audioHook.test.ts b/src/components/pages/gameData/story/audioHook.test.ts new file mode 100644 index 00000000..87d6c3be --- /dev/null +++ b/src/components/pages/gameData/story/audioHook.test.ts @@ -0,0 +1,162 @@ +import {renderReactHook} from '../../../../../test/render/main'; +import {StoryConversation} from '../../../../api-def/resources'; +import {useAudioControl} from './audioHook'; + +describe('Audio control hook', () => { + const conversations: Array = [ + { + type: 'conversation', + speakerName: 'speaker', + speakerIcon: null, + content: 'content', + isSys: false, + audioPaths: ['A'], + }, + { + type: 'conversation', + speakerName: 'speaker', + speakerIcon: null, + content: 'content', + isSys: false, + audioPaths: ['B'], + }, + { + type: 'conversation', + speakerName: 'speaker', + speakerIcon: null, + content: 'content', + isSys: false, + audioPaths: [], + }, + {type: 'break'}, + { + type: 'conversation', + speakerName: 'speaker', + speakerIcon: null, + content: 'content', + isSys: false, + audioPaths: ['C'], + }, + ]; + + it('is not playing on load', async () => { + const {result} = renderReactHook(() => useAudioControl({ + getConversationOfIndex: (idx) => conversations[idx], + conversationCount: conversations.length, + })); + + expect(result.current.currentState.mainIdx).toBe(-1); + expect(result.current.currentState.subIdx).toBe(-1); + expect(result.current.currentState.isPlaying).toBeFalsy(); + expect(result.current.playingState).toBe('stopping'); + }); + + it('starts', async () => { + const {result} = renderReactHook(() => useAudioControl({ + getConversationOfIndex: (idx) => conversations[idx], + conversationCount: conversations.length, + })); + + result.current.startAudio(); + + expect(result.current.currentState.mainIdx).toBe(0); + expect(result.current.currentState.subIdx).toBe(0); + expect(result.current.currentState.isPlaying).toBeTruthy(); + expect(result.current.playingState).toBe('playing'); + }); + + it('pauses', async () => { + const {result} = renderReactHook(() => useAudioControl({ + getConversationOfIndex: (idx) => conversations[idx], + conversationCount: conversations.length, + })); + + result.current.startAudio(); + result.current.advanceToNextAudio(0)(); + result.current.pauseAudio(); + + expect(result.current.currentState.mainIdx).toBe(1); + expect(result.current.currentState.subIdx).toBe(0); + expect(result.current.currentState.isPlaying).toBeFalsy(); + expect(result.current.playingState).toBe('pausing'); + }); + + it('resumes', async () => { + const {result} = renderReactHook(() => useAudioControl({ + getConversationOfIndex: (idx) => conversations[idx], + conversationCount: conversations.length, + })); + + result.current.startAudio(); + result.current.advanceToNextAudio(0)(); + result.current.pauseAudio(); + result.current.resumeAudio(); + + expect(result.current.currentState.mainIdx).toBe(1); + expect(result.current.currentState.subIdx).toBe(0); + expect(result.current.currentState.isPlaying).toBeTruthy(); + expect(result.current.playingState).toBe('playing'); + }); + + it('stops', async () => { + const {result} = renderReactHook(() => useAudioControl({ + getConversationOfIndex: (idx) => conversations[idx], + conversationCount: conversations.length, + })); + + result.current.startAudio(); + result.current.advanceToNextAudio(0)(); + result.current.advanceToNextAudio(1)(); + result.current.stopAudio(); + + expect(result.current.currentState.mainIdx).toBe(-1); + expect(result.current.currentState.subIdx).toBe(-1); + expect(result.current.currentState.isPlaying).toBeFalsy(); + expect(result.current.playingState).toBe('stopping'); + }); + + it('advanced to the next sub-audio', async () => { + const {result} = renderReactHook(() => useAudioControl({ + getConversationOfIndex: (idx) => conversations[idx], + conversationCount: conversations.length, + })); + + result.current.startAudio(); + result.current.advanceToNextSub(1); + + expect(result.current.currentState.mainIdx).toBe(0); + expect(result.current.currentState.subIdx).toBe(1); + expect(result.current.currentState.isPlaying).toBeTruthy(); + expect(result.current.playingState).toBe('playing'); + }); + + it('does not stop on break', async () => { + const {result} = renderReactHook(() => useAudioControl({ + getConversationOfIndex: (idx) => conversations[idx], + conversationCount: conversations.length, + })); + + result.current.startAudio(); + result.current.advanceToNextAudio(2)(); + + expect(result.current.currentState.mainIdx).toBe(4); + expect(result.current.currentState.subIdx).toBe(0); + expect(result.current.currentState.isPlaying).toBeTruthy(); + expect(result.current.playingState).toBe('playing'); + }); + + it('goes to the next conversation if no label', async () => { + const {result} = renderReactHook(() => useAudioControl({ + getConversationOfIndex: (idx) => conversations[idx], + conversationCount: conversations.length, + })); + + result.current.startAudio(); + result.current.advanceToNextAudio(1)(); + + expect(result.current.currentState.mainIdx).toBe(4); + expect(result.current.currentState.subIdx).toBe(0); + expect(result.current.currentState.isPlaying).toBeTruthy(); + expect(result.current.playingState).toBe('playing'); + }); +}); diff --git a/src/components/pages/gameData/story/audioHook.ts b/src/components/pages/gameData/story/audioHook.ts new file mode 100644 index 00000000..124ba6ab --- /dev/null +++ b/src/components/pages/gameData/story/audioHook.ts @@ -0,0 +1,78 @@ +import React from 'react'; + +import {StoryConversation} from '../../../../api-def/resources'; +import {AudioPlayingState, AudioState, UseAudioControlReturn} from './types'; + + +type UseAudioControlOptions = { + getConversationOfIndex: (index: number) => StoryConversation, + conversationCount: number +} + +export const useAudioControl = ({ + getConversationOfIndex, + conversationCount, +}: UseAudioControlOptions): UseAudioControlReturn => { + const [state, setState] = React.useState({ + mainIdx: -1, + subIdx: -1, + isPlaying: false, + }); + + let playingState: AudioPlayingState; + if (state.mainIdx === -1) { + playingState = 'stopping'; + } else { + playingState = state.isPlaying ? 'playing' : 'pausing'; + } + + const isPlayingForIdx = (idx: number) => state.mainIdx === idx; + + const startAudio = () => setState({mainIdx: 0, subIdx: 0, isPlaying: true}); + + const stopAudio = () => setState({mainIdx: -1, subIdx: -1, isPlaying: false}); + + const pauseAudio = () => setState({...state, isPlaying: false}); + + const resumeAudio = () => setState({...state, isPlaying: true}); + + const hasAudioToPlay = (idx: number) => { + const conversation = getConversationOfIndex(idx); + + if (conversation.type !== 'conversation') { + return false; + } + + return conversation.audioPaths.length > 0; + }; + + const advanceToNextAudio = (currentAudioIdx: number) => () => { + let newAudioIdx = currentAudioIdx + 1; + + while (!hasAudioToPlay(newAudioIdx)) { + newAudioIdx++; + + if (newAudioIdx >= conversationCount) { + // Audio loop complete, set back to non-playing index + stopAudio(); + return; + } + } + + setState({mainIdx: newAudioIdx, subIdx: 0, isPlaying: true}); + }; + + const advanceToNextSub = (subIdx: number) => setState({...state, subIdx}); + + return { + playingState, + isPlayingForIdx, + startAudio, + stopAudio, + pauseAudio, + resumeAudio, + advanceToNextAudio, + advanceToNextSub, + currentState: state, + }; +}; diff --git a/src/components/pages/gameData/story/chapter.tsx b/src/components/pages/gameData/story/chapter.tsx index feee6988..a7a994c1 100644 --- a/src/components/pages/gameData/story/chapter.tsx +++ b/src/components/pages/gameData/story/chapter.tsx @@ -1,6 +1,9 @@ import React from 'react'; + import {Story} from '../../../../api-def/resources'; +import {AudioControl} from './audioControl'; +import {useAudioControl} from './audioHook'; import {StoryBreak} from './conversation/break'; import {StoryTalk} from './conversation/talk'; @@ -10,11 +13,28 @@ type Props = { } export const StoryChapter = ({chapter}: Props) => { + const hookReturn = useAudioControl({ + getConversationOfIndex: (idx) => chapter.conversations[idx], + conversationCount: chapter.conversations.length, + }); + const {playingState, isPlayingForIdx, advanceToNextAudio, advanceToNextSub, currentState} = hookReturn; + return ( <> + {chapter.conversations.map((conversation, idx) => { if (conversation.type === 'conversation') { - return ; + return ( + + ); } if (conversation.type === 'break') { return ; diff --git a/src/components/pages/gameData/story/conversation/talk.tsx b/src/components/pages/gameData/story/conversation/talk.tsx index f75e6e79..3fe0528d 100644 --- a/src/components/pages/gameData/story/conversation/talk.tsx +++ b/src/components/pages/gameData/story/conversation/talk.tsx @@ -1,17 +1,24 @@ import React from 'react'; +import ReactAudioPlayer from 'react-audio-player'; import Col from 'react-bootstrap/Col'; import Row from 'react-bootstrap/Row'; -import {DepotPaths, StoryTalk as StoryTalkData} from '../../../../../api-def/resources'; +import {AudioPaths, DepotPaths, StoryTalk as StoryTalkData} from '../../../../../api-def/resources'; import {Image} from '../../../../elements/common/image'; import styles from '../main.module.css'; -import {StoryConversationProps} from './types'; -type Props = StoryConversationProps +type Props = { + conversation: StoryTalkData, + playAudio: boolean, + audioIdx: number, + isActive: boolean, + setAudioIdx: (newAudioIdx: number) => void, + onAllAudioPlayed: () => void, +} -export const StoryTalk = ({conversation}: Props) => { +export const StoryTalk = ({conversation, playAudio, isActive, audioIdx, setAudioIdx, onAllAudioPlayed}: Props) => { if (conversation.isSys) { return ( @@ -23,7 +30,22 @@ export const StoryTalk = ({conversation}: Props) => { } return ( - <> +
+ { + playAudio && conversation.audioPaths.length > 0 && audioIdx < conversation.audioPaths.length && + { + const newAudioIdx = audioIdx + 1; + setAudioIdx(newAudioIdx); + + if (newAudioIdx >= conversation.audioPaths.length) { + onAllAudioPlayed(); + } + }} + /> + } @@ -31,7 +53,7 @@ export const StoryTalk = ({conversation}: Props) => { - + { conversation.speakerIcon ? @@ -47,6 +69,6 @@ export const StoryTalk = ({conversation}: Props) => { {conversation.content} - +
); }; diff --git a/src/components/pages/gameData/story/conversation/types.ts b/src/components/pages/gameData/story/conversation/types.ts deleted file mode 100644 index af26a67d..00000000 --- a/src/components/pages/gameData/story/conversation/types.ts +++ /dev/null @@ -1,6 +0,0 @@ -import {StoryConversation} from '../../../../../api-def/resources'; - - -export type StoryConversationProps = { - conversation: T -} diff --git a/src/components/pages/gameData/story/main.module.css b/src/components/pages/gameData/story/main.module.css index 7004878a..de9c575b 100644 --- a/src/components/pages/gameData/story/main.module.css +++ b/src/components/pages/gameData/story/main.module.css @@ -3,6 +3,7 @@ div.sysMessage { border-radius: 0.25rem; text-align: center; padding: 0.5rem; + margin: 0.5rem 0; } div.speakerIcon { height: 3rem; @@ -15,6 +16,30 @@ div.mainImage { div.relatedLinks { text-align: center; } +div.audioControl { + text-align: center; + margin: 0.5rem 0; +} +@keyframes conversationBackground { + 0% { + background-color: #312901; + } + 50% { + background-color: #473a06; + } + 100% { + background-color: #312901; + } +} +div.conversation, div.conversationActive { + margin: 0.3rem 0; +} +div.conversationActive { + animation: conversationBackground 3s linear infinite; + border: #806805 solid 3px; + border-radius: 0.5rem; + padding: 0 0.3rem 0.3rem; +} img.speakerIcon { border-radius: 0.25rem; @@ -23,4 +48,15 @@ img.mainImage { max-height: 60vh; } +button.audioControl { + display: flex; + justify-content: center; + align-items: center; + font-size: 2rem; + width: 3rem; + height: 3rem; + border: 0; + margin: 0 auto; +} + /*# sourceMappingURL=main.module.css.map */ diff --git a/src/components/pages/gameData/story/main.module.css.map b/src/components/pages/gameData/story/main.module.css.map index fa31d293..c6617459 100644 --- a/src/components/pages/gameData/story/main.module.css.map +++ b/src/components/pages/gameData/story/main.module.css.map @@ -1 +1 @@ -{"version":3,"sourceRoot":"","sources":["main.module.scss"],"names":[],"mappings":"AAGE;EACE;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;;AAGF;EACE;;AAGF;EACE;;;AAKF;EACE;;AAGF;EACE","file":"main.module.css"} \ No newline at end of file +{"version":3,"sourceRoot":"","sources":["main.module.scss"],"names":[],"mappings":"AAGE;EACE;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;;AAGF;EACE;;AAGF;EACE;;AAGF;EACE;EACA;;AAGF;EACE;IACE;;EAGF;IACE;;EAGF;IACE;;;AAIJ;EACE;;AAGF;EAGE;EACA;EACA;EACA;;;AAKF;EACE;;AAGF;EACE;;;AAKF;EAEE;EACA;EACA;EAEA;EACA;EACA;EAEA;EACA","file":"main.module.css"} \ No newline at end of file diff --git a/src/components/pages/gameData/story/main.module.scss b/src/components/pages/gameData/story/main.module.scss index bdec1c25..da7f0bc2 100644 --- a/src/components/pages/gameData/story/main.module.scss +++ b/src/components/pages/gameData/story/main.module.scss @@ -6,6 +6,7 @@ div { border-radius: 0.25rem; text-align: center; padding: 0.5rem; + margin: 0.5rem 0; } &.speakerIcon { @@ -21,6 +22,38 @@ div { &.relatedLinks { text-align: center; } + + &.audioControl { + text-align: center; + margin: 0.5rem 0; + } + + @keyframes conversationBackground { + 0% { + background-color: #312901; + } + + 50% { + background-color: #473a06; + } + + 100% { + background-color: #312901; + } + } + + &.conversation { + margin: 0.3rem 0; + } + + &.conversationActive { + @extend .conversation; + + animation: conversationBackground 3s linear infinite; + border: #806805 solid 3px; + border-radius: 0.5rem; + padding: 0 0.3rem 0.3rem; + } } img { @@ -32,3 +65,19 @@ img { max-height: 60vh; } } + +button { + &.audioControl { + // For centering icon + display: flex; + justify-content: center; + align-items: center; + // Sizing + font-size: 2rem; + width: 3rem; + height: 3rem; + // Styling + border: 0; + margin: 0 auto; + } +} diff --git a/src/components/pages/gameData/story/types.ts b/src/components/pages/gameData/story/types.ts new file mode 100644 index 00000000..c795edc6 --- /dev/null +++ b/src/components/pages/gameData/story/types.ts @@ -0,0 +1,19 @@ +export type AudioState = { + mainIdx: number, + subIdx: number, + isPlaying: boolean, +} + +export type AudioPlayingState = 'playing' | 'pausing' | 'stopping'; + +export type UseAudioControlReturn = { + playingState: AudioPlayingState, + isPlayingForIdx: (idx: number) => boolean, + startAudio: () => void, + stopAudio: () => void, + pauseAudio: () => void, + resumeAudio: () => void, + advanceToNextAudio: (srcIdx: number) => () => void, + advanceToNextSub: (newSubIdx: number) => void, + currentState: AudioState, +} From 82ff927c16aa06f7faac7d3430c461712b18eea7 Mon Sep 17 00:00:00 2001 From: RaenonX Date: Wed, 22 Sep 2021 05:08:28 -0500 Subject: [PATCH 12/19] ADD - Story voice play auto-scroll #296 Signed-off-by: RaenonX --- src/components/pages/gameData/story/conversation/talk.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/pages/gameData/story/conversation/talk.tsx b/src/components/pages/gameData/story/conversation/talk.tsx index 3fe0528d..7212d119 100644 --- a/src/components/pages/gameData/story/conversation/talk.tsx +++ b/src/components/pages/gameData/story/conversation/talk.tsx @@ -19,6 +19,8 @@ type Props = { } export const StoryTalk = ({conversation, playAudio, isActive, audioIdx, setAudioIdx, onAllAudioPlayed}: Props) => { + const ref = React.useRef(null); + if (conversation.isSys) { return ( @@ -29,8 +31,12 @@ export const StoryTalk = ({conversation, playAudio, isActive, audioIdx, setAudio ); } + if (playAudio) { + ref.current?.scrollIntoView({behavior: 'smooth', block: 'center', inline: 'center'}); + } + return ( -
+
{ playAudio && conversation.audioPaths.length > 0 && audioIdx < conversation.audioPaths.length && Date: Wed, 22 Sep 2021 10:14:26 -0500 Subject: [PATCH 13/19] FIX - floating number issue #309 Signed-off-by: RaenonX --- package.json | 1 + .../unitInfo/output/elements/normalAttack/branchedTab.tsx | 8 ++++---- src/utils/calc.ts | 6 ++++++ 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 69d4b169..11068e8f 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "@reduxjs/toolkit": "^1.6.1", "bootstrap": "^4.6.0", "color": "^4.0.1", + "decimal.js": "^10.3.1", "fastify": "^3.21.0", "mathjs": "^9.4.4", "mongodb": "^3.7.0", diff --git a/src/components/pages/gameData/unitInfo/output/elements/normalAttack/branchedTab.tsx b/src/components/pages/gameData/unitInfo/output/elements/normalAttack/branchedTab.tsx index 76369b4b..589033e5 100644 --- a/src/components/pages/gameData/unitInfo/output/elements/normalAttack/branchedTab.tsx +++ b/src/components/pages/gameData/unitInfo/output/elements/normalAttack/branchedTab.tsx @@ -3,7 +3,7 @@ import React from 'react'; import {NormalAttackBranchedChain} from '../../../../../../../api-def/resources'; import {ConditionCodes} from '../../../../../../../const/gameData'; import {useI18n} from '../../../../../../../i18n/hook'; -import {sum} from '../../../../../../../utils/calc'; +import {roundArray, sum} from '../../../../../../../utils/calc'; import {ConditionBadges} from '../../../../../../elements/gameData/badges/conditions'; import {Markdown} from '../../../../../../elements/markdown/main'; import styles from '../main.module.css'; @@ -46,7 +46,7 @@ export const NormalAttackBranchedTab = ({branchedChain}: Props) => { {idx + 1} - {`==(${combo.mods.join(' + ')}) x 100%[2f]==`} + {`==(${roundArray(combo.mods, 3).join(' + ')}) x 100%[2f]==`} {combo.mods.length} @@ -54,14 +54,14 @@ export const NormalAttackBranchedTab = ({branchedChain}: Props) => { {branchedChain.hasUtp && {combo.utp}} - {`==(${combo.odRate.join(' + ')}) / ${combo.odRate.length}[2f]==`} + {`==(${roundArray(combo.odRate, 3).join(' + ')}) / ${combo.odRate.length}[2f]==`} { branchedChain.hasCrisis && - {`==(${combo.crisisMod.join(' + ')}) / ${combo.crisisMod.length}[2f]==`} + {`==(${roundArray(combo.crisisMod, 3).join(' + ')}) / ${combo.crisisMod.length}[2f]==`} } diff --git a/src/utils/calc.ts b/src/utils/calc.ts index 66e7493d..a89e9af5 100644 --- a/src/utils/calc.ts +++ b/src/utils/calc.ts @@ -1,3 +1,5 @@ +import Decimal from 'decimal.js'; + export const normalize = (numbers: Array, padding: number = 0) => { const max = Math.max(...numbers); @@ -22,3 +24,7 @@ export const varTally = (arr: Array) => { return tally; }; + +export const roundArray = (nums: Array, places: number): Array => { + return nums.map((mod) => new Decimal(mod).toDecimalPlaces(places, Decimal.ROUND_HALF_CEIL)); +}; From e05279a9ef0e2c7dd415163cbf4b10ea48eb76bb Mon Sep 17 00:00:00 2001 From: RaenonX Date: Wed, 22 Sep 2021 10:16:20 -0500 Subject: [PATCH 14/19] IMP - CHT translation for unit info index Signed-off-by: RaenonX --- src/i18n/translations/cht/translation.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/i18n/translations/cht/translation.ts b/src/i18n/translations/cht/translation.ts index c9fa2dd2..4a712ae0 100644 --- a/src/i18n/translations/cht/translation.ts +++ b/src/i18n/translations/cht/translation.ts @@ -655,7 +655,7 @@ export const translation: TranslationStruct = { }, gameData: { info: { - title: '角色/龍族資訊目錄', + title: '角色/龍族索引', description: '各角色、龍族的評測、資訊的索引頁面。', }, ex: { @@ -709,7 +709,7 @@ export const translation: TranslationStruct = { suffix: ' | 龍絆攻略站 by OM', }, nav: { - unitInfo: '角色/龍族資訊', + unitInfo: '角色/龍族索引', unitTier: '評級', }, posts: { From 70500d1a4600eaec1b6bb7b742a5995674c49e58 Mon Sep 17 00:00:00 2001 From: RaenonX Date: Wed, 22 Sep 2021 10:47:26 -0500 Subject: [PATCH 15/19] IMP - Improve style of the collapsible section title #297 Signed-off-by: RaenonX --- .../elements/posts/output/section.module.css | 13 +++++++++++++ .../elements/posts/output/section.module.css.map | 1 + .../elements/posts/output/section.module.scss | 16 ++++++++++++++++ .../elements/posts/output/section.test.tsx | 2 +- src/components/elements/posts/output/section.tsx | 13 +++++-------- 5 files changed, 36 insertions(+), 9 deletions(-) create mode 100644 src/components/elements/posts/output/section.module.css create mode 100644 src/components/elements/posts/output/section.module.css.map create mode 100644 src/components/elements/posts/output/section.module.scss diff --git a/src/components/elements/posts/output/section.module.css b/src/components/elements/posts/output/section.module.css new file mode 100644 index 00000000..0be4dde4 --- /dev/null +++ b/src/components/elements/posts/output/section.module.css @@ -0,0 +1,13 @@ +div.sectionTitle { + background-color: #323232; + align-items: center; + margin: 0.5rem 0; + padding: 0.5rem 0; + border-radius: 0.5rem; +} +div.sectionTitle:hover { + background-color: #505050; + cursor: pointer; +} + +/*# sourceMappingURL=section.module.css.map */ diff --git a/src/components/elements/posts/output/section.module.css.map b/src/components/elements/posts/output/section.module.css.map new file mode 100644 index 00000000..9694aac4 --- /dev/null +++ b/src/components/elements/posts/output/section.module.css.map @@ -0,0 +1 @@ +{"version":3,"sourceRoot":"","sources":["section.module.scss","../../../../../styles/colors.scss"],"names":[],"mappings":"AAGE;EACE,kBCEa;EDDb;EACA;EACA;EACA;;AAGF;EACE,kBCLa;EDMb","file":"section.module.css"} \ No newline at end of file diff --git a/src/components/elements/posts/output/section.module.scss b/src/components/elements/posts/output/section.module.scss new file mode 100644 index 00000000..b6dcbc40 --- /dev/null +++ b/src/components/elements/posts/output/section.module.scss @@ -0,0 +1,16 @@ +@use "../../../../../styles/colors"; + +div { + &.sectionTitle { + background-color: colors.$color-black-50; + align-items: center; + margin: 0.5rem 0; + padding: 0.5rem 0; + border-radius: 0.5rem; + } + + &.sectionTitle:hover { + background-color: colors.$color-black-80; + cursor: pointer; + } +} diff --git a/src/components/elements/posts/output/section.test.tsx b/src/components/elements/posts/output/section.test.tsx index 2ac22e4c..cda0cbd3 100644 --- a/src/components/elements/posts/output/section.test.tsx +++ b/src/components/elements/posts/output/section.test.tsx @@ -34,7 +34,7 @@ describe('Collapsible sections', () => { /> )); - const buttonOpenA = screen.getAllByText(translationEN.misc.collapse)[0]; + const buttonOpenA = screen.getByText('a'); userEvent.click(buttonOpenA); expect(screen.getByText('a')).toBeInTheDocument(); diff --git a/src/components/elements/posts/output/section.tsx b/src/components/elements/posts/output/section.tsx index a36e7815..c2bd8ed9 100644 --- a/src/components/elements/posts/output/section.tsx +++ b/src/components/elements/posts/output/section.tsx @@ -6,6 +6,8 @@ import Collapse from 'react-bootstrap/Collapse'; import Row from 'react-bootstrap/Row'; import {useI18n} from '../../../../i18n/hook'; +import {IconCollapse} from '../../common/icons'; +import styles from './section.module.css'; type Props = { @@ -41,14 +43,9 @@ export const CollapsibleSectionedContent = ({sections, getTitle, renderSect return ( - - -
{title}
- - - + + setOpen({...open, [title]: !open[title]})}> +
 {title}
From 67037e0c225e20b3ca94955a05ffb729bff01a86 Mon Sep 17 00:00:00 2001 From: RaenonX Date: Wed, 22 Sep 2021 18:08:01 -0500 Subject: [PATCH 16/19] FIX - Undercover Grace story 1 no sound #312 Signed-off-by: RaenonX --- .../pages/gameData/story/audioHook.test.ts | 41 +++++++++++++++++++ .../pages/gameData/story/audioHook.ts | 34 +++++++-------- 2 files changed, 58 insertions(+), 17 deletions(-) diff --git a/src/components/pages/gameData/story/audioHook.test.ts b/src/components/pages/gameData/story/audioHook.test.ts index 87d6c3be..fcc8a71d 100644 --- a/src/components/pages/gameData/story/audioHook.test.ts +++ b/src/components/pages/gameData/story/audioHook.test.ts @@ -159,4 +159,45 @@ describe('Audio control hook', () => { expect(result.current.currentState.isPlaying).toBeTruthy(); expect(result.current.playingState).toBe('playing'); }); + + it('starts from the 1st audible audio label', async () => { + const conversationsOverride: Array = [ + { + type: 'conversation', + speakerName: 'speaker', + speakerIcon: null, + content: 'content', + isSys: false, + audioPaths: [], + }, + { + type: 'conversation', + speakerName: 'speaker', + speakerIcon: null, + content: 'content', + isSys: false, + audioPaths: [], + }, + { + type: 'conversation', + speakerName: 'speaker', + speakerIcon: null, + content: 'content', + isSys: false, + audioPaths: ['A'], + }, + ]; + + const {result} = renderReactHook(() => useAudioControl({ + getConversationOfIndex: (idx) => conversationsOverride[idx], + conversationCount: conversationsOverride.length, + })); + + result.current.startAudio(); + + expect(result.current.currentState.mainIdx).toBe(2); + expect(result.current.currentState.subIdx).toBe(0); + expect(result.current.currentState.isPlaying).toBeTruthy(); + expect(result.current.playingState).toBe('playing'); + }); }); diff --git a/src/components/pages/gameData/story/audioHook.ts b/src/components/pages/gameData/story/audioHook.ts index 124ba6ab..d6e3cadd 100644 --- a/src/components/pages/gameData/story/audioHook.ts +++ b/src/components/pages/gameData/story/audioHook.ts @@ -28,7 +28,23 @@ export const useAudioControl = ({ const isPlayingForIdx = (idx: number) => state.mainIdx === idx; - const startAudio = () => setState({mainIdx: 0, subIdx: 0, isPlaying: true}); + const advanceToNextAudio = (currentAudioIdx: number) => () => { + let newAudioIdx = currentAudioIdx + 1; + + while (!hasAudioToPlay(newAudioIdx)) { + newAudioIdx++; + + if (newAudioIdx >= conversationCount) { + // Audio loop complete, set back to non-playing index + stopAudio(); + return; + } + } + + setState({mainIdx: newAudioIdx, subIdx: 0, isPlaying: true}); + }; + + const startAudio = advanceToNextAudio(-1); const stopAudio = () => setState({mainIdx: -1, subIdx: -1, isPlaying: false}); @@ -46,22 +62,6 @@ export const useAudioControl = ({ return conversation.audioPaths.length > 0; }; - const advanceToNextAudio = (currentAudioIdx: number) => () => { - let newAudioIdx = currentAudioIdx + 1; - - while (!hasAudioToPlay(newAudioIdx)) { - newAudioIdx++; - - if (newAudioIdx >= conversationCount) { - // Audio loop complete, set back to non-playing index - stopAudio(); - return; - } - } - - setState({mainIdx: newAudioIdx, subIdx: 0, isPlaying: true}); - }; - const advanceToNextSub = (subIdx: number) => setState({...state, subIdx}); return { From deaae34188d6c4e26da762d285394c7f4cc498a0 Mon Sep 17 00:00:00 2001 From: RaenonX Date: Wed, 22 Sep 2021 20:21:22 -0500 Subject: [PATCH 17/19] ADD - Skeleton for single unit tier note page #301 Signed-off-by: RaenonX --- pages/[lang]/tier/[id].ts | 4 ++++ src/components/pages/tier/unit/main.tsx | 6 ++++++ src/const/path/definitions.ts | 1 + src/i18n/translations/cht/translation.ts | 4 ++++ src/i18n/translations/definition.ts | 1 + src/i18n/translations/en/translation.ts | 4 ++++ src/i18n/translations/jp/translation.ts | 4 ++++ src/utils/meta/translations.ts | 1 + 8 files changed, 25 insertions(+) create mode 100644 pages/[lang]/tier/[id].ts create mode 100644 src/components/pages/tier/unit/main.tsx diff --git a/pages/[lang]/tier/[id].ts b/pages/[lang]/tier/[id].ts new file mode 100644 index 00000000..6fae641c --- /dev/null +++ b/pages/[lang]/tier/[id].ts @@ -0,0 +1,4 @@ +import {TierNoteUnit} from '../../../src/components/pages/tier/unit/main'; + + +export default TierNoteUnit; diff --git a/src/components/pages/tier/unit/main.tsx b/src/components/pages/tier/unit/main.tsx new file mode 100644 index 00000000..b11c4afd --- /dev/null +++ b/src/components/pages/tier/unit/main.tsx @@ -0,0 +1,6 @@ +import React from 'react'; + + +export const TierNoteUnit = () => { + return <>WIP; +}; diff --git a/src/const/path/definitions.ts b/src/const/path/definitions.ts index 63cc4fd8..fc3dba98 100644 --- a/src/const/path/definitions.ts +++ b/src/const/path/definitions.ts @@ -15,6 +15,7 @@ export enum StoryPath { // Must and only have `id` as the key for unit ID export enum UnitPath { UNIT_INFO = '/info/[id]', + UNIT_TIER = '/tier/[id]', UNIT_TIER_EDIT = '/tier/edit/[id]' } diff --git a/src/i18n/translations/cht/translation.ts b/src/i18n/translations/cht/translation.ts index 4a712ae0..4bc967cc 100644 --- a/src/i18n/translations/cht/translation.ts +++ b/src/i18n/translations/cht/translation.ts @@ -652,6 +652,10 @@ export const translation: TranslationStruct = { description: '編輯要點內容的頁面。', }, }, + unit: { + title: '【評級 / 要點】{{unitName}}', + description: '{{unitName}} 的評級和要點頁面。', + }, }, gameData: { info: { diff --git a/src/i18n/translations/definition.ts b/src/i18n/translations/definition.ts index e2a005e7..dd2440a1 100644 --- a/src/i18n/translations/definition.ts +++ b/src/i18n/translations/definition.ts @@ -530,6 +530,7 @@ export type TranslationStruct = { usage: PageMetaTranslations, edit: PageMetaTranslations, }, + unit: PageMetaTranslations, }, gameData: { info: PageMetaTranslations, diff --git a/src/i18n/translations/en/translation.ts b/src/i18n/translations/en/translation.ts index 53a86a42..c4a770ea 100644 --- a/src/i18n/translations/en/translation.ts +++ b/src/i18n/translations/en/translation.ts @@ -699,6 +699,10 @@ export const translation: TranslationStruct = { description: 'Page to edit the content of the key points.', }, }, + unit: { + title: '【Ranking / Key Point】{{unitName}}', + description: 'Ranking and the key points of {{unitName}} .', + }, }, gameData: { info: { diff --git a/src/i18n/translations/jp/translation.ts b/src/i18n/translations/jp/translation.ts index 86df9a74..c3167f6c 100644 --- a/src/i18n/translations/jp/translation.ts +++ b/src/i18n/translations/jp/translation.ts @@ -657,6 +657,10 @@ export const translation: TranslationStruct = { description: 'ポイントの編集ページ。', }, }, + unit: { + title: 'TBA', + description: 'TBA', + }, }, gameData: { info: { diff --git a/src/utils/meta/translations.ts b/src/utils/meta/translations.ts index f19522e2..df0ac2a3 100644 --- a/src/utils/meta/translations.ts +++ b/src/utils/meta/translations.ts @@ -7,6 +7,7 @@ export const metaTransFunctions: { [path in PagePath]: GetTranslationFunction t.meta.inUse.tier.points.usage, [UnitPath.UNIT_INFO]: (t) => t.meta.inUse.unit.info, + [UnitPath.UNIT_TIER]: (t) => t.meta.inUse.tier.unit, [UnitPath.UNIT_TIER_EDIT]: (t) => t.meta.inUse.tier.edit, [PostPath.QUEST]: (t) => t.meta.inUse.post.quest.post, [PostPath.QUEST_EDIT]: (t) => t.meta.inUse.post.quest.edit, From e64c71566345d06352188ab8e414b9ceb0f196c8 Mon Sep 17 00:00:00 2001 From: RaenonX Date: Wed, 22 Sep 2021 20:25:20 -0500 Subject: [PATCH 18/19] ADD - Link to the tier note page of a unit #301 Signed-off-by: RaenonX --- src/components/elements/gameData/unit/link.test.tsx | 4 ++++ src/components/elements/gameData/unit/link.tsx | 8 ++++++++ src/i18n/translations/cht/translation.ts | 1 + src/i18n/translations/definition.ts | 1 + src/i18n/translations/en/translation.ts | 1 + src/i18n/translations/jp/translation.ts | 1 + 6 files changed, 16 insertions(+) diff --git a/src/components/elements/gameData/unit/link.test.tsx b/src/components/elements/gameData/unit/link.test.tsx index 3e1c935e..51fc138e 100644 --- a/src/components/elements/gameData/unit/link.test.tsx +++ b/src/components/elements/gameData/unit/link.test.tsx @@ -30,6 +30,10 @@ describe('Unit link', () => { const expectedAnalysisLink = makePostUrl(PostPath.ANALYSIS, {pid: 10950101, lang: SupportedLanguages.EN}); expect(analysisLink).toHaveAttribute('href', expectedAnalysisLink); + const tierLink = screen.getByText('Ranking / Tier'); + const expectedTierLink = makeUnitUrl(UnitPath.UNIT_TIER, {id: 10950101, lang: SupportedLanguages.EN}); + expect(tierLink).toHaveAttribute('href', expectedTierLink); + const infoLink = screen.getByText('Info'); const expectedInfoLink = makeUnitUrl(UnitPath.UNIT_INFO, {id: 10950101, lang: SupportedLanguages.EN}); expect(infoLink).toHaveAttribute('href', expectedInfoLink); diff --git a/src/components/elements/gameData/unit/link.tsx b/src/components/elements/gameData/unit/link.tsx index 82e7be39..ac066281 100644 --- a/src/components/elements/gameData/unit/link.tsx +++ b/src/components/elements/gameData/unit/link.tsx @@ -52,6 +52,14 @@ const ModalContent = ({unit, hasAnalysis, modalState, setModalState}: ModalConte /> } +