+
+ {$t(`series.${series}`)}{' '}
+ {$t('season', {
+ s: season,
+ })}
+
+ {_range(1, length + 1).map((chapter) => {
+ const currentSelection = chapter === selected
+ return (
+
+ )
+ })}
+
+ )
+}
+
+export default SeasonChapterList
diff --git a/components/stories/SpecialStoriesItem.tsx b/components/stories/SpecialStoriesItem.tsx
new file mode 100644
index 00000000..7565551a
--- /dev/null
+++ b/components/stories/SpecialStoriesItem.tsx
@@ -0,0 +1,26 @@
+import { useTranslations } from 'next-intl'
+
+import { toVideoLink } from '#components/ExternalVideo'
+import { ChapterItem } from '#data/types'
+
+const SpecialStoriesItem = ({ item }: { item: ChapterItem }) => {
+ const $c = useTranslations('common')
+ const { name, video } = item
+
+ return (
+ <>
+
- {$t(`series.${series}`)} {$t('season', { season })} -{' '}
+ {$t(`series.${series}`)} {$t('season', { s: season })} -{' '}
{chapter}
- {cnTitle !== null ? (
+ {localTitle !== null ? (
<>
-
{cnTitle} /{' '}
+
{localTitle} /{' '}
{jaTitle}
@@ -115,10 +95,10 @@ const StoriesItem = (props: PropType) => {
{StoryData.description}
)}
- {data && (
+ {StoryTrnData && (
- 尚无{data === undefined ? '剧情' : '标题'}
- 翻译信息。请添加翻译信息到{' '}
-
- data/stories.data.ts
- {' '}
- 的 StoriesData[{series}][{season}][{chapter}] 。
+ {(StoryTrnData === undefined || localTitle === null) && (
+
+ {$c.rich('no_trans', {
+ field: `StoriesData[${series}][${season}][${chapter}]`,
+ file: 'data/stories.data.ts',
+ })}
)}
diff --git a/components/stories/getSpecialStories.ts b/components/stories/getSpecialStories.ts
new file mode 100644
index 00000000..85e03b8c
--- /dev/null
+++ b/components/stories/getSpecialStories.ts
@@ -0,0 +1,7 @@
+import storiesData from '#data/videos/stories.data'
+
+const getSpecialStories = (locale: string) => {
+ return storiesData?.[locale]?.special
+}
+
+export default getSpecialStories
diff --git a/data/cardStories.data.ts b/data/cardStories.data.ts
index 2cbae56e..0676bc76 100644
--- a/data/cardStories.data.ts
+++ b/data/cardStories.data.ts
@@ -968,6 +968,21 @@ const dataRio: D = {
},
phone: { video: { type: 'bilibili', vid: 'av467970608' } },
},
+ 'card-rio-05-kiok-00': {
+ // 心に刻む星月夜
+ 1: {
+ name: '回忆的巡游',
+ video: { type: 'bilibili', vid: 'av943673121' },
+ },
+ 2: {
+ name: '想知道你的事',
+ video: { type: 'bilibili', vid: 'av388827513' },
+ },
+ 3: {
+ name: '在星的海洋之中',
+ video: { type: 'bilibili', vid: 'av431366423' },
+ },
+ },
}
// 井川葵
diff --git a/data/profile.data/en.json b/data/profile.data/en.json
new file mode 100644
index 00000000..9e26dfee
--- /dev/null
+++ b/data/profile.data/en.json
@@ -0,0 +1 @@
+{}
\ No newline at end of file
diff --git a/data/profile.data/index.ts b/data/profile.data/index.ts
new file mode 100644
index 00000000..edd06dee
--- /dev/null
+++ b/data/profile.data/index.ts
@@ -0,0 +1,11 @@
+import en from './en.json'
+import zhHans from './zh-hans.json'
+
+import { CharacterId } from '#data/vendor/characterId'
+
+const _: Record
> = {
+ en: en,
+ 'zh-hans': zhHans,
+}
+
+export default _
diff --git a/data/profile.data/zh-hans.json b/data/profile.data/zh-hans.json
new file mode 100644
index 00000000..c75ec84c
--- /dev/null
+++ b/data/profile.data/zh-hans.json
@@ -0,0 +1,20 @@
+{
+ "char-ktn": "继承姐姐长濑麻奈的遗志,立志成为偶像的少女. 在组合成立之初,也有刻意避开与人交流而喜欢独处的时候. 但最终在和成员一同跨越苦难的过程中,同大家一起成长,并且成为了能够引领大家的存在.",
+ "char-ngs": "为了支持挚友琴乃而成为偶像的少女. 想象力丰富,经常会在所有人意想不到的地方表现出强烈的好奇心. 虽然曾因为没有作为偶像的优点而失去过自信,但是为了成为一股能够让琴乃依靠的力量,每天都有在奋斗.",
+ "char-ski": "她是学校里的学生会长,经常被学生们说是如同画中走出来的优等生. 她还是月光风暴中像妈妈一样的角色. 在出道之初,也曾为自己对妹妹千纱的过度保护而烦恼. 但现在相互信赖彼此,也终于是能够独立前行了.",
+ "char-suz": "极其推崇麻奈,属于是冒失且看起来容易被(芽衣)欺负的大小姐. 因为被父母强迫留学,所以带着为了“逃离父母”的心思去了事务所. 之后,努力克服了自己的逃避心理,并成功得到了父母的正式认可,这也使得星见事务所的大家为之高兴了很长一段时间.",
+ "char-mei": "遵从直觉而活、天真烂漫的少女. 抱着“看起来很有趣”的单纯动机接受了邀请并进入了事务所. 在经历了各种事情之后被麻奈作为偶像的态度所感动,最终开始认真地以她那样的偶像为目标并向前迈进.",
+ "char-skr": "一个天真烂漫、珍惜每一天的少女. 能够从过去的经历中感受到内心的微妙,还拥有着能够让人依偎的温柔.",
+ "char-szk": "她是一名不擅长笑容的沉默寡言型御宅偶像. 在加入星见事务所之初,对缺乏语言和感情表达能力而感到不安,差点失去了作为偶像的自信. 但是,与有着同样烦恼的千纱意气相投,在每一天的互相支持中,二人一同成长着.",
+ "char-chs": "是一名有着胆小怕事性格的偶像. 为了改变那样的自己,她立志成为偶像. 一开始只能跟在姐姐后面的她,通过偶像活动渐渐地增强了自信. 最终下定了和姐姐一起用自己的步伐向前迈进的决心.",
+ "char-rei": "拥有着足以在舞蹈比赛中获得第一名的实力. 为了让父母认可把舞蹈作为工作的她,便立志成为偶像. 加入事务所之初,虽然也曾对成员的水平要求很高,但最终也是找到了用自己所拥有的实力引导大家前行的道路.",
+ "char-hrk": "不仅是团队中的大姐姐,也是Sunny peace的中坚角色. 她还认识生前的麻奈,也是星见事务所最初的所属偶像. 虽然暂时放弃了梦想,但出于不想背叛相信自己的人的心情,以强烈的觉悟努力进行偶像活动.",
+ "char-rui": "TRINITYAiLE的绝对核心. 虽然被誉为天才,但实际情况却是以压倒性的努力为基础的. 从凛然的身姿容易被认为是完美无缺的. 不过也有突然睡着,偏食的一面.",
+ "char-yu": "出身于京都,总是保持冷静的TRINITYAiLE的润滑剂和仲裁者. 父亲是大企业的总裁,母亲则是著名女演员萨拉布雷德小姐. 被瑠依的个人魅力所迷住,经常将她放在内心的首位而行动.",
+ "char-smr": "性格坚定得让人无法想象是中学生的前天才童星. 虽然每日往返于偏远的老家山形县,但依旧不辞劳苦地向他人露出笑容. 平时就和中学生一样洋溢着青春感,一旦放松下来就会不自觉地说出方言.",
+ "char-rio": "总是对胜利充满渴望,个人风格充斥着攻击性,是LizNoir永远的核心. 曾经历过一段时间的活动休止期,但后来为了斩断和某人之间的命运而再次投身偶像活动. 抱着比任何人都要强大的觉悟,向偶像事业的顶点发起挑战.",
+ "char-aoi": "感情起伏少,在任何状况都能始终保持冷静的LizNoir军师,同时具备着看一遍舞蹈就能完美复制的能力,是强劲的舞蹈实力派. 总是以利落又冷酷的态度表达自己的想法,跟出道前就同甘共苦的神崎莉央是互相知心的战友.",
+ "char-ai": "最喜欢让他人露出微笑,是LizNoir的气氛调节者. 具备相当的偶像能力却容易在关键时刻表现出令人意想不到的笨拙. 不擅长开玩笑,对赤崎心说的话总是盲目的相信.",
+ "char-kkr": "温柔却有时得意忘形,这便是LizNoir的突击队长——赤崎心. 偶尔会发表一些让人捉摸不透的看法,时常令他人感到深不可测. 和小美山爱是互相认可的同伴,但经常给爱灌输不好的思想.",
+ "char-kan": "十分表里不一,但直觉异常敏锐的小恶魔偶像. 从小作为大牌模特,出现在许多杂志的封面. 极度渴望被人认可,SNS账号有200万以上的粉丝."
+}
\ No newline at end of file
diff --git a/data/stories.ts b/data/stories.ts
index acf99734..0a1506d2 100644
--- a/data/stories.ts
+++ b/data/stories.ts
@@ -7,7 +7,6 @@ export const Series = [
'Mana',
'ThreeX',
'Tsuki',
- 'Special',
] as const
export type SeriesName = typeof Series[number]
@@ -29,5 +28,4 @@ export const Episodes: Record = {
ThreeX: [20],
// adv_group_moon_
Tsuki: [10],
- Special: [3],
}
diff --git a/data/videos/eventStories.data/zh-hans.ts b/data/videos/eventStories.data/zh-hans.ts
index 095c7dba..3f9d13b7 100644
--- a/data/videos/eventStories.data/zh-hans.ts
+++ b/data/videos/eventStories.data/zh-hans.ts
@@ -245,6 +245,7 @@ const data: Record = {
name: '只要是我们的话,就没有问题',
video: { type: 'bilibili', vid: 'av553882116' },
},
+ // 熱中☆ハプニングサマー
'st-eve-2208-backside-001': {
name: '想和前辈一起玩',
video: { type: 'bilibili', vid: 'av301706967' },
@@ -265,6 +266,7 @@ const data: Record = {
name: '比夏天更炎热的恋爱体验',
video: { type: 'bilibili', vid: 'av344304719' },
},
+ // 未来とつながるマジカルメロディ
'st-eve-2209-contest-001': {
name: '虚拟歌手',
video: { type: 'bilibili', vid: 'av260183330' },
@@ -285,6 +287,27 @@ const data: Record = {
name: 'with. 初音未来',
video: { type: 'bilibili', vid: 'av857781292' },
},
+ // 運命繋ぐ流星の軌跡
+ 'st-eve-2210-race-001': {
+ name: '追忆的天空',
+ video: { type: 'bilibili', vid: 'av688850892', pid: 1 },
+ },
+ 'st-eve-2210-race-002': {
+ name: '琴乃的请求',
+ video: { type: 'bilibili', vid: 'av688850892', pid: 2 },
+ },
+ 'st-eve-2210-race-003': {
+ name: '星见的惊喜',
+ video: { type: 'bilibili', vid: 'av688850892', pid: 3 },
+ },
+ 'st-eve-2210-race-004': {
+ name: '交汇的命运',
+ video: { type: 'bilibili', vid: 'av688850892', pid: 4 },
+ },
+ 'st-eve-2210-race-005': {
+ name: '星之海的记忆',
+ video: { type: 'bilibili', vid: 'av688850892', pid: 5 },
+ },
}
export const eventGroup: Record = {
diff --git a/data/videos/stories.data/en.ts b/data/videos/stories.data/en.ts
new file mode 100644
index 00000000..6db17eed
--- /dev/null
+++ b/data/videos/stories.data/en.ts
@@ -0,0 +1,21 @@
+import { StoriesData } from './types'
+
+const special: StoriesData['special'] = []
+
+const data: StoriesData['data'] = {
+ Hoshimi: {},
+ Tokyo: {},
+ TRINITYAiLE: {},
+ LizNoir: {},
+ Mana: {},
+ ThreeX: {},
+ Big4: {},
+ Tsuki: {},
+}
+
+const _ = {
+ data,
+ special,
+}
+
+export default _
diff --git a/data/videos/stories.data/index.ts b/data/videos/stories.data/index.ts
new file mode 100644
index 00000000..cb438cbe
--- /dev/null
+++ b/data/videos/stories.data/index.ts
@@ -0,0 +1,10 @@
+import en from './en'
+import zhHans from './zh-hans'
+import type { StoriesData } from './types'
+
+const _: Record = {
+ en: en,
+ 'zh-hans': zhHans,
+}
+
+export default _
diff --git a/data/videos/stories.data/types.d.ts b/data/videos/stories.data/types.d.ts
new file mode 100644
index 00000000..a726dbeb
--- /dev/null
+++ b/data/videos/stories.data/types.d.ts
@@ -0,0 +1,12 @@
+import type { SeriesName } from '#data/stories'
+import { ChapterItem } from '#data/types'
+
+export type IStoriesData = Record<
+ SeriesName,
+ Record>
+>
+
+export type StoriesData = {
+ data: IStoriesData
+ special: ChapterItem[]
+}
diff --git a/data/stories.data.ts b/data/videos/stories.data/zh-hans.ts
similarity index 96%
rename from data/stories.data.ts
rename to data/videos/stories.data/zh-hans.ts
index 52c95251..b72a65f4 100644
--- a/data/stories.data.ts
+++ b/data/videos/stories.data/zh-hans.ts
@@ -1,38 +1,34 @@
-import type { SeriesName } from './stories'
-import { ChapterItem } from './types'
+import type { StoriesData } from './types'
-const data: Partial<
- Record>>
-> = {
- // 其它剧情
- Special: {
- 1: {
- 1: {
- name: '手游开篇剧情',
- video: { type: 'bilibili', vid: 'av761315570' },
- },
- 2: {
- name: '[2022/4/1] 愚人节春斗剧情(前篇)',
- video: { type: 'bilibili', vid: 'av980357709', pid: 1 },
- },
- 3: {
- name: '[2022/4/1] 愚人节春斗剧情(后篇)',
- video: { type: 'bilibili', vid: 'av980357709', pid: 2 },
- },
- 4: {
- name: '[2022/6/30] 三得利售货机限定慰问来电(瑠依)',
- video: { type: 'bilibili', vid: 'av812917702' },
- },
- 5: {
- name: '[2022/6/30] 三得利售货机限定慰问来电(堇)',
- video: { type: 'bilibili', vid: 'av813293495' },
- },
- 6: {
- name: '[2022/6/30] 三得利售货机限定慰问来电(优)',
- video: { type: 'bilibili', vid: 'av343290005' },
- },
- },
+// 其它剧情
+const special: StoriesData['special'] = [
+ {
+ name: '手游开篇剧情',
+ video: { type: 'bilibili', vid: 'av761315570' },
+ },
+ {
+ name: '[2022/4/1] 愚人节春斗剧情(前篇)',
+ video: { type: 'bilibili', vid: 'av980357709', pid: 1 },
+ },
+ {
+ name: '[2022/4/1] 愚人节春斗剧情(后篇)',
+ video: { type: 'bilibili', vid: 'av980357709', pid: 2 },
},
+ {
+ name: '[2022/6/30] 三得利售货机限定慰问来电(瑠依)',
+ video: { type: 'bilibili', vid: 'av812917702' },
+ },
+ {
+ name: '[2022/6/30] 三得利售货机限定慰问来电(堇)',
+ video: { type: 'bilibili', vid: 'av813293495' },
+ },
+ {
+ name: '[2022/6/30] 三得利售货机限定慰问来电(优)',
+ video: { type: 'bilibili', vid: 'av343290005' },
+ },
+]
+
+const data: StoriesData['data'] = {
Hoshimi: {
1: {
1: {
@@ -1185,4 +1181,6 @@ const data: Partial<
},
}
-export default data
+const _ = { data, special }
+
+export default _
diff --git a/data/wikiPages/fetch.ts b/data/wikiPages/fetch.ts
index 8f268c31..3bb133c8 100755
--- a/data/wikiPages/fetch.ts
+++ b/data/wikiPages/fetch.ts
@@ -40,7 +40,6 @@ const CharacterChineseNameList: Record = {
'char-mku': '初音未来',
} as const
-const IdolsJson = 'idols.json'
const CardsJson = 'cards.json'
const SongsJson = 'songs.json'
const SitePref: SitePrefConfig = {
@@ -139,25 +138,6 @@ function parseIdol(pageJson: any) {
async function main() {
const currDir = fileURLToPath(dirname(import.meta.url))
- try {
- // Idols
- const idolInfo = readJson(join(currDir, IdolsJson))
- for (const [idolId, idolCnName] of Object.entries(
- CharacterChineseNameList
- )) {
- if (idolInfo?.[idolId]) {
- console.info(`Skipping idol ${idolCnName}`)
- continue
- }
- console.info(`Fetching idol ${idolCnName}`)
- const pageJson = await getPageJson(idolCnName, SitePref)
- idolInfo[idolId] = parseIdol(pageJson)
- }
- writeFileSync(join(currDir, IdolsJson), JSON.stringify(idolInfo))
- } catch (e) {
- console.error(`Failed to update idol data: ${e}`)
- }
-
try {
// Cards
const cardInfo = readJson(join(currDir, CardsJson))
diff --git a/data/wikiPages/idols.d.ts b/data/wikiPages/idols.d.ts
deleted file mode 100644
index 1969074b..00000000
--- a/data/wikiPages/idols.d.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-/**
- * Idol data for wiki.biligame.com/idolypride
- */
-export type TheRootSchema = Record
-/**
- * Information for an idol
- */
-export interface IdolInfo {
- nameJa?: string
- nameCn?: string
- birthday?: string
- team?:
- | '月のテンペスト'
- | 'サニーピース'
- | '星见事务所'
- | 'TRINITYAiLE'
- | 'LizNoir'
- cv?: string
- desc?: string
- [k: string]: unknown
-}
diff --git a/data/wikiPages/idols.json b/data/wikiPages/idols.json
deleted file mode 100644
index 5fb8942b..00000000
--- a/data/wikiPages/idols.json
+++ /dev/null
@@ -1 +0,0 @@
-{"char-ktn":{"nameJa":"長瀬琴乃","nameCn":"长濑琴乃","birthday":"12月25日","team":"月のテンペスト","cv":"橘美来","desc":"继承姐姐长濑麻奈的遗志,立志成为偶像的少女. \n在组合成立之初,也有刻意避开与人交流而喜欢独处的时候. 但最终在和成员一同跨越苦难的过程中,同大家一起成长,\n并且成为了能够引领大家的存在."},"char-ngs":{"nameJa":"伊吹渚","nameCn":"伊吹渚","birthday":"8月3日","team":"月のテンペスト","cv":"夏目ここな","desc":"为了支持挚友琴乃而成为偶像的少女. \n想象力丰富,经常会在所有人意想不到的地方表现出强烈的好奇心. \n虽然曾因为没有作为偶像的优点而失去过自信,但是为了成为一股能够让琴乃依靠的力量,每天都有在奋斗."},"char-ski":{"nameJa":"白石沙季","nameCn":"白石沙季","birthday":"9月26日","team":"月のテンペスト","cv":"宮沢小春","desc":"她是学校里的学生会长,经常被学生们说是如同画中走出来的优等生. 她还是月光风暴中像妈妈一样的角色. \n在出道之初,也曾为自己对妹妹千纱的过度保护而烦恼. 但现在相互信赖彼此,也终于是能够独立前行了."},"char-suz":{"nameJa":"成宮すず","nameCn":"成宫铃","birthday":"9月13日","team":"月のテンペスト","cv":"相川奏多","desc":"极其推崇麻奈,属于是冒失且看起来容易被(芽衣)欺负的大小姐. \n因为被父母强迫留学,所以带着为了“逃离父母”的心思去了事务所. \n之后,努力克服了自己的逃避心理,并成功得到了父母的正式认可,这也使得星见事务所的大家为之高兴了很长一段时间."},"char-mei":{"nameJa":"早坂芽衣","nameCn":"早坂芽衣","birthday":"7月7日","team":"月のテンペスト","cv":"日向もか","desc":"遵从直觉而活、天真烂漫的少女. \n抱着“看起来很有趣”的单纯动机接受了邀请并进入了事务所. \n在经历了各种事情之后被麻奈作为偶像的态度所感动,最终开始认真地以她那样的偶像为目标并向前迈进."},"char-skr":{"nameJa":"川咲さくら","nameCn":"川咲樱","birthday":"4月3日","team":"サニーピース","cv":"菅野真衣","desc":"一个天真烂漫、珍惜每一天的少女. 能够从过去的经历中感受到内心的微妙,还拥有着能够让人依偎的温柔."},"char-szk":{"nameJa":"兵藤雫","nameCn":"兵藤雫","birthday":"10月15日","team":"サニーピース","cv":"首藤志奈","desc":"她是一名不擅长笑容的沉默寡言型御宅偶像. \n\n在加入星见事务所之初,对缺乏语言和感情表达能力而感到不安,差点失去了作为偶像的自信. \n\n但是,与有着同样烦恼的千纱意气相投,在每一天的互相支持中,二人一同成长着."},"char-chs":{"nameJa":"白石千紗","nameCn":"白石千纱","birthday":"11月22日","team":"サニーピース","cv":"高尾奏音","desc":"是一名有着胆小怕事性格的偶像. \n\n为了改变那样的自己,她立志成为偶像. \n\n一开始只能跟在姐姐后面的她,通过偶像活动渐渐地增强了自信. 最终下定了和姐姐一起用自己的步伐向前迈进的决心."},"char-rei":{"nameJa":"一ノ瀬怜","nameCn":"一之濑怜","birthday":"3月8日","team":"サニーピース","cv":"結城萌子","desc":"拥有着足以在舞蹈比赛中获得第一名的实力. \n\n为了让父母认可把舞蹈作为工作的她,便立志成为偶像. \n\n加入事务所之初,虽然也曾对成员的水平要求很高,但最终也是找到了用自己所拥有的实力引导大家前行的道路."},"char-hrk":{"nameJa":"佐伯遙子","nameCn":"佐伯遥子","birthday":"1月3日","team":"サニーピース","cv":"佐々木奈緒","desc":"不仅是团队中的大姐姐,也是Sunny peace的中坚角色. \n\n她还认识生前的麻奈,也是星见事务所最初的所属偶像. \n\n虽然暂时放弃了梦想,但出于不想背叛相信自己的人的心情,以强烈的觉悟努力进行偶像活动."},"char-rui":{"nameJa":"天動瑠依","nameCn":"天动瑠依","birthday":"11月11日","team":"TRINITYAiLE","cv":"雨宮 天","desc":"TRINITYAiLE的绝对核心. 虽然被誉为天才,但实际情况却是以压倒性的努力为基础的. 从凛然的身姿容易被认为是完美无缺的. 不过也有突然睡着,偏食的一面."},"char-yu":{"nameJa":"鈴村優","nameCn":"铃村优","birthday":"2月27日","team":"TRINITYAiLE","cv":"麻仓桃","desc":"出身于京都,总是保持冷静的TRINITYAiLE的润滑剂和仲裁者. 父亲是大企业的总裁,母亲则是著名女演员萨拉布雷德小姐. 被瑠依的个人魅力所迷住,经常将她放在内心的首位而行动."},"char-smr":{"nameJa":"奥山すみれ","nameCn":"奥山堇","birthday":"5月5日","team":"TRINITYAiLE","cv":"夏川椎菜","desc":"性格坚定得让人无法想象是中学生的前天才童星. 虽然每日往返于偏远的老家山形县,但依旧不辞劳苦地向他人露出笑容. 平时就和中学生一样洋溢着青春感,一旦放松下来就会不自觉地说出方言."},"char-rio":{"nameJa":"神崎莉央","nameCn":"神崎莉央","birthday":"8月28日","team":"LizNoir","cv":"户松遥","desc":"总是对胜利充满渴望,个人风格充斥着攻击性,是LizNoir永远的核心. 曾经历过一段时间的活动休止期,但后来为了斩断和某人之间的命运而再次投身偶像活动. 抱着比任何人都要强大的觉悟,向偶像事业的顶点发起挑战."},"char-aoi":{"nameJa":"井川葵","nameCn":"井川葵","birthday":"6月19日","team":"LizNoir","cv":"高垣彩阳","desc":"感情起伏少,在任何状况都能始终保持冷静的LizNoir军师,同时具备着看一遍舞蹈就能完美复制的能力,是强劲的舞蹈实力派. 总是以利落又冷酷的态度表达自己的想法,跟出道前就同甘共苦的神崎莉央是互相知心的战友."},"char-ai":{"nameJa":"小美山愛","nameCn":"小美山爱","birthday":"2月9日","team":"LizNoir","cv":"寿美菜子","desc":"最喜欢让他人露出微笑,是LizNoir的气氛调节者. 具备相当的偶像能力却容易在关键时刻表现出令人意想不到的笨拙. 不擅长开玩笑,对赤崎心说的话总是盲目的相信."},"char-kkr":{"nameJa":"赤崎こころ","nameCn":"赤崎心","birthday":"12月6日","team":"LizNoir","cv":"丰崎爱生","desc":"温柔却有时得意忘形,这便是LizNoir的突击队长——赤崎心. 偶尔会发表一些让人捉摸不透的看法,时常令他人感到深不可测. 和小美山爱是互相认可的同伴,但经常给爱灌输不好的思想."},"char-mna":{"nameJa":"長瀬麻奈","nameCn":"长濑麻奈","birthday":"10月9日","team":"星见事务所","cv":"神田沙也加","desc":"そこにいるだけで、周囲を笑顔にしてくれるアイドル. 手が届かないようなスター性を感じさせる一方で、飾らない親しみやすさも持ち合わせる稀有な存在."},"char-kor":{"nameJa":"fran","nameCn":"fran","birthday":"6月11日","team":"IIIX","cv":"Lynn","desc":"抜群のスタイルを持つ、天才肌で努力を知らないカリスマ. ⅢⅩのセンターで、パリコレへの出場経験もあるトップモデル出身. ある理由から極端にお金に執着している."},"char-kan":{"nameJa":"kana","nameCn":"kana","birthday":"4月10日","team":"IIIX","cv":"田中爱美","desc":"十分表里不一,但直觉异常敏锐的小恶魔偶像. 从小作为大牌模特,出现在许多杂志的封面. 极度渴望被人认可,SNS账号有200万以上的粉丝."},"char-mhk":{"nameJa":"miho","nameCn":"miho","birthday":"1月25日","team":"IIIX","cv":"村川梨衣","desc":"豊富な経験と知識を活かして活動に励む理論派. BIG4に手が届く寸前で突如解散した、人気二人組アイドルのメンバーだった過去を持つ. グループのブレーン的存在で基本的に温厚だが、執念深い一面も."},"char-mku":{"nameJa":"初音ミク","nameCn":"初音未来","birthday":"8月31日","team":"—","cv":"藤田咲","desc":"世代を超えて愛される、ポップでキュートなバーチャル・シンガー. VENUSプログラムとは無縁の舞台で活躍しており、その明るく美しい歌声は、世界中の人々を魅了している."}}
\ No newline at end of file
diff --git a/data/wikiPages/index.ts b/data/wikiPages/index.ts
index 42a7bc85..d5eb2354 100644
--- a/data/wikiPages/index.ts
+++ b/data/wikiPages/index.ts
@@ -1,12 +1,9 @@
import _cards from './cards.json' assert { type: 'json' }
-import _idols from './idols.json' assert { type: 'json' }
import _songs from './songs.json' assert { type: 'json' }
import _wikiPagesMeta from './meta.json' assert { type: 'json' }
import type { TheRootSchema as WikiCards } from './cards'
-import type { TheRootSchema as WikiIdols } from './idols'
import type { TheRootSchema as WikiSongs } from './songs'
export const Cards = _cards as WikiCards
-export const Idols = _idols as WikiIdols
export const Songs = _songs as WikiSongs
export const Meta = _wikiPagesMeta
diff --git a/locales/en/about.json b/locales/en/about.json
new file mode 100644
index 00000000..693c77df
--- /dev/null
+++ b/locales/en/about.json
@@ -0,0 +1,16 @@
+{
+ "About": "About",
+ "Contributors:": "Contributors:",
+ "site_desc": "INFO PRIDE is a info site for Project IDOLY PRIDE fans/players.",
+ "loading_contribs": "Loading contributors...",
+ "emoji_a11y": "Accessibility",
+ "emoji_audio": "Audio",
+ "emoji_blog": "Blog",
+ "emoji_bug": "Bug",
+ "emoji_code": "Code",
+ "emoji_content": "Content",
+ "emoji_data": "Data",
+ "emoji_design": "Design",
+ "emoji_doc": "Documentation",
+ "emoji_translation": "Translation"
+}
\ No newline at end of file
diff --git a/locales/en/colors.json b/locales/en/colors.json
new file mode 100644
index 00000000..e49cf432
--- /dev/null
+++ b/locales/en/colors.json
@@ -0,0 +1,4 @@
+{
+ "Colors": "Colors",
+ "colors_header": "The colors come from in-game data. Also for reference: Image from official Twitter."
+}
\ No newline at end of file
diff --git a/locales/en/common.json b/locales/en/common.json
index 01c393c3..f00d3d5e 100644
--- a/locales/en/common.json
+++ b/locales/en/common.json
@@ -2,7 +2,7 @@
"sidebar": {
"Stories": "Stories",
"Event Stories": "Event Stories",
- "Notemaps": "Notemaps",
+ "Notemaps": "Beatmaps",
"Characters": "Characters",
"Cards": "Cards",
"Units": "Units",
@@ -25,11 +25,11 @@
}
},
"Video": "Video",
- "Loading...": "Loading...",
"Back": "Back",
"Color": "Color",
+ "loading": "Loading...",
"audio_loading": "Audio is loading - note that the first load may be longer.",
"audio_load_failed": "Failed to load audio {id}.",
- "no_trans": "Translationed subbed video is not found. Please add them to {field} at {file}.",
+ "no_trans": "Translation info is not found. Please add them to {field} at {file}.",
"load_progress": "Loading data... [{cur}/{total}]"
}
\ No newline at end of file
diff --git a/locales/en/eventstories.json b/locales/en/eventstories.json
index be43744b..22b59737 100644
--- a/locales/en/eventstories.json
+++ b/locales/en/eventstories.json
@@ -1,5 +1,5 @@
{
- "Event Stories": "活动剧情",
+ "Event Stories": "Event Stories",
"stories_ordering": "Event stories are ordered from latest to oldest.",
"episode": "Episode {ep}",
"Event banner": "Event banner"
diff --git a/locales/en/settings.json b/locales/en/settings.json
new file mode 100644
index 00000000..50e4af42
--- /dev/null
+++ b/locales/en/settings.json
@@ -0,0 +1,10 @@
+{
+ "Settings": "Settings",
+ "My box": "My box",
+ "Save": "Save",
+ "Success": "Success",
+ "Your settings have been saved.": "Your settings have been saved.",
+ "Browser compatiblity problem": "Browser compatiblity problem",
+ "localstorage_unsupported": "The browser doesn't support localStorage. Please update your browser or switch to normal mode to save settings.",
+ "mybox_header": "After setting the box, card possession status will be shown while searching."
+}
diff --git a/locales/en/stories.json b/locales/en/stories.json
index ef577e01..99811391 100644
--- a/locales/en/stories.json
+++ b/locales/en/stories.json
@@ -1,4 +1,8 @@
{
+ "Stories": "Stories",
+ "Others": "Others",
+ "Video": "Video",
+ "season": "Season {s}",
"series": {
"Hoshimi": "Hoshimi Series",
"Tokyo": "Tokyo Series",
@@ -9,6 +13,5 @@
"ThreeX": "ⅢⅩ",
"Tsuki": "Tsuki no Tempest",
"Special": "Others"
- },
- "season": "Season {s}"
+ }
}
\ No newline at end of file
diff --git a/locales/zh-hans/about.json b/locales/zh-hans/about.json
new file mode 100644
index 00000000..713dc929
--- /dev/null
+++ b/locales/zh-hans/about.json
@@ -0,0 +1,16 @@
+{
+ "About": "关于",
+ "Contributors:": "贡献者们:",
+ "site_desc": "INFO PRIDE 是一个为 IDOLY PRIDE 企划同好及游戏玩家设计的信息站点。",
+ "loading_contribs": "正在加载贡献列表...",
+ "emoji_a11y": "可访问性",
+ "emoji_audio": "音频",
+ "emoji_blog": "文章",
+ "emoji_bug": "问题报告",
+ "emoji_code": "代码",
+ "emoji_content": "内容",
+ "emoji_data": "数据",
+ "emoji_design": "设计",
+ "emoji_doc": "文档",
+ "emoji_translation": "翻译"
+}
\ No newline at end of file
diff --git a/locales/zh-hans/characters.json b/locales/zh-hans/characters.json
index c3c3ad2e..0a305597 100644
--- a/locales/zh-hans/characters.json
+++ b/locales/zh-hans/characters.json
@@ -97,13 +97,13 @@
"age": "{age} 岁",
"Birthday stories": "生日剧情",
"Year": "年份",
- "bday-opening": "开场剧情",
- "bday-phone": "电话剧情",
- "bday-others": "全员祝福",
- "no-bday-translation": "暂无生日剧情翻译信息。请添加到翻译信息到 data/birthday.data.ts 的 BirthdayCommu[{charaId}] 。",
"In-game voices": "游戏语音",
"Title": "标题",
"After title": "标题后半",
"Gifts": "礼物",
- "Store": "商店"
+ "Store": "商店",
+ "bday-opening": "开场剧情",
+ "bday-phone": "电话剧情",
+ "bday-others": "全员祝福",
+ "no-bday-translation": "暂无生日剧情翻译信息。请添加到翻译信息到 data/birthday.data.ts 的 BirthdayCommu[{charaId}] 。"
}
\ No newline at end of file
diff --git a/locales/zh-hans/colors.json b/locales/zh-hans/colors.json
new file mode 100644
index 00000000..955188c0
--- /dev/null
+++ b/locales/zh-hans/colors.json
@@ -0,0 +1,4 @@
+{
+ "Colors": "系列颜色",
+ "colors_header": "此颜色根据游戏内部数据得到。亦可参考官方 Twitter提供的图片。"
+}
\ No newline at end of file
diff --git a/locales/zh-hans/common.json b/locales/zh-hans/common.json
index 3893f901..0ee7bb5e 100644
--- a/locales/zh-hans/common.json
+++ b/locales/zh-hans/common.json
@@ -25,12 +25,12 @@
}
},
"Video": "视频",
- "Loading...": "加载中。",
"Back": "返回",
"Color": "颜色",
"Export as image": "导出为图片",
+ "loading": "加载中。",
"audio_loading": "正在加载音频。首次加载可能需要较长时间。",
"audio_load_failed": "未能加载音频 {id} 。",
- "no_trans": "尚无剧情翻译信息。请添加翻译信息到 {file} 的 {field} 。",
+ "no_trans": "尚无翻译信息。请添加翻译信息到 {file} 的 {field} 。",
"load_progress": "正在加载数据:完成 {cur}/{total}"
}
\ No newline at end of file
diff --git a/locales/zh-hans/events.json b/locales/zh-hans/events.json
index e9204d96..9e1010f0 100644
--- a/locales/zh-hans/events.json
+++ b/locales/zh-hans/events.json
@@ -1,4 +1,5 @@
{
+ "運命繋ぐ流星の軌跡": "命运相连 流星之迹",
"未来とつながるマジカルメロディ": "连接未来的 Magical Melody",
"熱中☆ハプニングサマー": "热闹的☆夏天",
"Happy Smile Selfie": "Happy Smile Selfie",
diff --git a/locales/zh-hans/settings.json b/locales/zh-hans/settings.json
new file mode 100644
index 00000000..7abea511
--- /dev/null
+++ b/locales/zh-hans/settings.json
@@ -0,0 +1,10 @@
+{
+ "Settings": "设置",
+ "My box": "我的 box",
+ "Save": "保存",
+ "Success": "成功",
+ "Your settings have been saved.": "你的设置已经保存。",
+ "Browser compatiblity problem": "浏览器兼容性问题",
+ "localstorage_unsupported": "此浏览器不支持 localStorage。请升级至更新的浏览器或切换至一般模式以保存设置。",
+ "mybox_header": "在此设置 box 后,搜索时将会显示卡片的持有状态。"
+}
diff --git a/locales/zh-hans/stories.json b/locales/zh-hans/stories.json
index 2aa6c555..f8c3d5f3 100644
--- a/locales/zh-hans/stories.json
+++ b/locales/zh-hans/stories.json
@@ -1,6 +1,8 @@
{
"Stories": "剧情",
"Others": "其它",
+ "Video": "视频",
+ "season": "{s} 章",
"series": {
"Hoshimi": "星见编",
"Tokyo": "东京编",
diff --git a/package.json b/package.json
index 660c2e57..6c397665 100644
--- a/package.json
+++ b/package.json
@@ -50,6 +50,7 @@
"lodash": "^4.17.21",
"next": "12.3.0",
"next-intl": "^2.7.5",
+ "next-query-params": "^4.0.0",
"nextjs-progressbar": "^0.0.14",
"postcss": "^8.4.16",
"react": "^18.2.0",
@@ -57,7 +58,8 @@
"react-query": "^3.39.2",
"rfdc": "^1.3.0",
"screenfull": "^6.0.2",
- "tailwindcss": "^3.1.8"
+ "tailwindcss": "^3.1.8",
+ "use-query-params": "^2.1.1"
},
"devDependencies": {
"@next/bundle-analyzer": "^12.3.0",
diff --git a/pages/about.tsx b/pages/about.tsx
index 317ee250..3434d928 100644
--- a/pages/about.tsx
+++ b/pages/about.tsx
@@ -1,5 +1,6 @@
import { Avatar, Card, Grid } from '@mantine/core'
import { useEffect, useState } from 'react'
+import { useTranslations } from 'next-intl'
import type { Contributor } from '#components/api/contributors/types'
import Title from '#components/Title'
@@ -12,50 +13,22 @@ import getI18nProps from '#utils/getI18nProps'
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
// Adopted from https://github.com/all-contributors/cli/blob/e9c1f55beb2c18391a5d5f0c9e8243dc3f89ebe3/src/util/contribution-types.js
-const EmojiTypes = {
- a11y: {
- symbol: '️️️️♿️',
- description: '可访问性',
- },
- audio: {
- symbol: '🔊',
- description: '音频',
- },
- blog: {
- symbol: '📝',
- description: '文章',
- },
- bug: {
- symbol: '🐛',
- description: '问题报告',
- },
- code: {
- symbol: '💻',
- description: '代码',
- },
- content: {
- symbol: '🖋',
- description: '内容',
- },
- data: {
- symbol: '🔣',
- description: '数据',
- },
- design: {
- symbol: '🎨',
- description: '设计',
- },
- doc: {
- symbol: '📖',
- description: '文档',
- },
- translation: {
- symbol: '🌍',
- description: '翻译',
- },
+const EmojiTypes: Record = {
+ a11y: '️️️️♿️',
+ audio: '🔊',
+ blog: '📝',
+ bug: '🐛',
+ code: '💻',
+ content: '🖋',
+ data: '🔣',
+ design: '🎨',
+ doc: '📖',
+ translation: '🌍',
}
const ContributorBox = ({ contrib }: { contrib: Contributor }) => {
+ const $t = useTranslations('about')
+
const { name, login, avatar_url, profile, contributions } = contrib
return (
@@ -71,11 +44,11 @@ const ContributorBox = ({ contrib }: { contrib: Contributor }) => {
{contributions.map((c, key) => {
- const typ = EmojiTypes[c as keyof typeof EmojiTypes]
+ const typ = EmojiTypes[c]
if (typ) {
return (
-
- {typ.symbol}
+
+ {typ}
)
}
@@ -87,6 +60,8 @@ const ContributorBox = ({ contrib }: { contrib: Contributor }) => {
}
const AboutPage = () => {
+ const $t = useTranslations('about')
+
const [contribs, setContribs] = useState([])
useEffect(() => {
@@ -99,16 +74,13 @@ const AboutPage = () => {
}, [])
return (
<>
-
-
- INFO PRIDE 是一个为 IDOLY PRIDE
- 企划同好及游戏玩家设计的信息站点。
-
+
+ {$t('site_desc')}
{contribs.length === 0 ? (
- 正在加载贡献列表...
+ {$t('loading_contribs')}
) : (
<>
- 贡献者们:
+ {$t('Contributors:')}
{contribs.map((one, idx) => (
@@ -122,6 +94,6 @@ const AboutPage = () => {
)
}
-export const getStaticProps = getI18nProps()
+export const getStaticProps = getI18nProps(['about'])
export default AboutPage
diff --git a/pages/api/characters/profile.ts b/pages/api/characters/profile.ts
new file mode 100644
index 00000000..fed623e2
--- /dev/null
+++ b/pages/api/characters/profile.ts
@@ -0,0 +1,42 @@
+import { withSentry } from '@sentry/nextjs'
+import type { NextApiRequest, NextApiResponse } from 'next'
+
+import { FrontendAPIResponseMapping } from '#utils/useFrontendApi'
+import pickFirstOrOne from '#utils/pickFirstOrOne'
+import { DEFAULT_LANGUAGE } from '#utils/constants'
+import data from '#data/profile.data'
+
+const charactersProfile = async (
+ req: NextApiRequest,
+ res: NextApiResponse
+) => {
+ const q = req.query
+
+ if (!q.id || !q.locale) {
+ res.status(400).end()
+ return
+ }
+
+ const id = String(q.id)
+ const locale = q.locale ? pickFirstOrOne(q.locale) : DEFAULT_LANGUAGE
+
+ if (data?.[locale]?.[id]) {
+ // Cache for 7d
+ res.setHeader('Cache-Control', 'max-age=604800')
+ res.status(200).json({
+ profile: data?.[locale]?.[id],
+ })
+ return
+ }
+
+ res.status(404).end()
+ return
+}
+
+export default withSentry(charactersProfile)
+
+export const config = {
+ api: {
+ externalResolver: true,
+ },
+}
diff --git a/pages/api/stories.ts b/pages/api/stories.ts
new file mode 100644
index 00000000..2f63e906
--- /dev/null
+++ b/pages/api/stories.ts
@@ -0,0 +1,41 @@
+import { withSentry } from '@sentry/nextjs'
+import type { NextApiRequest, NextApiResponse } from 'next'
+
+import pickFirstOrOne from '#utils/pickFirstOrOne'
+import type { FrontendAPIResponseMapping } from '#utils/useFrontendApi'
+import { DEFAULT_LANGUAGE } from '#utils/constants'
+import storiesData from '#data/videos/stories.data'
+import { SeriesName } from '#data/stories'
+
+// ?series=hoshimi&season=1&chapter=1
+const eventStories = async (
+ req: NextApiRequest,
+ res: NextApiResponse
+) => {
+ const q = req.query
+ const series = q.series
+ const season = Number.parseInt(String(q.season) ?? '')
+ const chapter = Number.parseInt(String(q.chapter) ?? '')
+ const locale = q.locale ? pickFirstOrOne(q.locale) : DEFAULT_LANGUAGE
+
+ if (!series || Number.isNaN(season) || Number.isNaN(chapter)) {
+ res.status(400).end()
+ return
+ }
+
+ const ret =
+ storiesData?.[locale]?.data?.[series as SeriesName]?.[season]?.[chapter]
+ if (!ret) {
+ res.status(404).end()
+ return
+ }
+ res.status(200).json(ret)
+}
+
+export default withSentry(eventStories)
+
+export const config = {
+ api: {
+ externalResolver: true,
+ },
+}
diff --git a/pages/characters.tsx b/pages/characters.tsx
index eed10683..fc2898ee 100644
--- a/pages/characters.tsx
+++ b/pages/characters.tsx
@@ -78,7 +78,7 @@ const CharactersPage = ({
}
const SkeletonCharactersPage = () => {
- const $t = useTranslations('character')
+ const $t = useTranslations('characters')
const { data: CharacterListData } = useApi('Character/List')
const allData = {
diff --git a/pages/colors.tsx b/pages/colors.tsx
index b6cb8106..52f1e161 100644
--- a/pages/colors.tsx
+++ b/pages/colors.tsx
@@ -48,14 +48,18 @@ const ColorsPage = ({
CharacterList: APIResponseOf<'Character/List'>
}) => {
const $vc = useTranslations('v-chr')
+ const $t = useTranslations('colors')
+
return (
<>
- 此颜色根据游戏内部数据得到。亦可参考
-
- 官方 Twitter
- {' '}
- 提供的图片。
+ {$t.rich('colors_header', {
+ a: (c) => (
+
+ {c}
+
+ ),
+ })}
{ColorOrder.map((row, _i) => (
@@ -79,6 +83,7 @@ const ColorsPage = ({
const SkeletonColorsPage = () => {
const { data: CharacterList } = useApi('Character/List')
+ const $t = useTranslations('colors')
const allData = {
CharacterList,
@@ -86,7 +91,7 @@ const SkeletonColorsPage = () => {
return (
<>
-
+
{allFinished(allData) ? (
) : (
@@ -96,6 +101,6 @@ const SkeletonColorsPage = () => {
)
}
-export const getStaticProps = getI18nProps(['vendor', 'v-chr'])
+export const getStaticProps = getI18nProps(['colors', 'vendor', 'v-chr'])
export default SkeletonColorsPage
diff --git a/pages/settings.tsx b/pages/settings.tsx
index e86ed80a..b197a904 100644
--- a/pages/settings.tsx
+++ b/pages/settings.tsx
@@ -18,7 +18,9 @@ const clone = rfdc({
export type LocalBox = Partial
>
const SettingsPage = () => {
+ const $t = useTranslations('settings')
const $vc = useTranslations('v-chr')
+ const $c = useTranslations('common')
const [localBox, setLocalBox] = useState({})
const { data: Cards } = useFrontendApi('cards')
@@ -43,9 +45,8 @@ const SettingsPage = () => {
const saveLocalBox = () => {
if (!window.localStorage) {
showNotification({
- title: '浏览器兼容性问题',
- message:
- '此浏览器不支持 localStorage。请升级至更新的浏览器以保存设置。',
+ title: $t('Browser compatiblity problem'),
+ message: $t('localstorage_unsupported'),
color: 'red',
})
@@ -53,17 +54,17 @@ const SettingsPage = () => {
}
localStorage.setItem(LOCALSTORAGE_BOX_TAG, JSON.stringify(localBox))
showNotification({
- title: '成功',
- message: '你的设置已经保存。',
+ title: $t('Success'),
+ message: $t('Your settings have been saved.'),
color: 'green',
})
}
return (
<>
-
- 我的 box
- 在此设置 box 后,搜索时将会显示卡片的持有状态。
+
+ {$t('My box')}
+ {$t('mybox_header')}
{Cards ? (
{Object.entries(Cards).map(([name], _key) => (
@@ -102,15 +103,15 @@ const SettingsPage = () => {
))}
) : (
- 正在加载卡片列表。
+ {$c('loading')}
)}
>
)
}
-export const getStaticProps = getI18nProps(['v-chr'])
+export const getStaticProps = getI18nProps(['settings', 'v-chr'])
export default SettingsPage
diff --git a/pages/stories.tsx b/pages/stories.tsx
index 61711fa7..f312a86e 100644
--- a/pages/stories.tsx
+++ b/pages/stories.tsx
@@ -1,43 +1,39 @@
-import { useMemo } from 'react'
import { Button, Grid, Tabs } from '@mantine/core'
import _range from 'lodash/range'
-import { atomWithHash } from 'jotai/utils'
-import { useAtom } from 'jotai'
import { useTranslations } from 'next-intl'
+import { NumberParam, useQueryParams, withDefault } from 'use-query-params'
-import StoriesItem, {
- SpecialStoriesItem,
-} from '#components/stories/StoriesItem'
-import { Episodes, Series } from '#data/stories'
-import StoriesData from '#data/stories.data'
+import StoriesItem from '#components/stories/StoriesItem'
+import SpecialStoriesItem from '#components/stories/SpecialStoriesItem'
+import { Episodes, Series, SeriesName } from '#data/stories'
import Title from '#components/Title'
-import getI18nProps from '#utils/getI18nProps'
+import { addI18nMessages } from '#utils/getI18nProps'
+import storiesData from '#data/videos/stories.data'
+import { IStoriesData } from '#data/videos/stories.data/types'
+import SeasonChapterList from '#components/stories/SeasonChapterList'
+import getSpecialStories from '#components/stories/getSpecialStories'
+import { ChapterItem } from '#data/types'
+import withQueryParam from '#utils/withQueryParam'
-const seriesAtom = atomWithHash('series', 0)
-const seasonAtom = atomWithHash('season', 1)
-const chapterAtom = atomWithHash('chapter', 1)
+const SPECIAL_SERIES_TAG = 99
-const StoriesPage = () => {
+const StoriesPage = ({
+ completion,
+ special,
+}: {
+ completion: IStoriesData<0 | 1>
+ special: ChapterItem[]
+}) => {
const $t = useTranslations('stories')
- const [series, setSeries] = useAtom(seriesAtom)
- const [season, setSeason] = useAtom(seasonAtom)
- const [chapter, setChapter] = useAtom(chapterAtom)
- const seriesName = Series[series]
- const completion = useMemo(() => {
- const ret: Record>> = {}
- for (let i = 0; i < Series.length; i++) {
- const seriesSlug = Series[i]
- ret[seriesSlug] = Episodes[seriesSlug].map((length, episodeKey) =>
- _range(1, length + 1).map((chapterId) =>
- Boolean(
- StoriesData?.[seriesSlug]?.[episodeKey + 1]?.[chapterId]
- )
- )
- )
- }
- return ret
- }, [])
+ const [query, setQuery] = useQueryParams({
+ series: withDefault(NumberParam, 0),
+ s: withDefault(NumberParam, 1),
+ c: withDefault(NumberParam, 1),
+ })
+ const { series: curSeries, s: curSeason, c: curChapter } = query
+
+ const seriesName = Series[curSeries]
return (
<>
@@ -46,126 +42,82 @@ const StoriesPage = () => {
- {Series.filter((x) => x !== 'Special').map(
- (seriesSlug, seriesKey) => (
-
- {$t(`series.${seriesSlug}`)}
-
- )
- )}
- {$t('Others')}
+ {Series.map((seriesSlug, seriesKey) => (
+
+ {$t(`series.${seriesSlug}`)}
+
+ ))}
+ {$t('Others')}
- {Series.filter((x) => x !== 'Special').map(
- (seriesSlug, seriesKey) => (
+ {Series.map((seriesSlug, seriesKey) => {
+ return (
{Episodes[seriesSlug].map(
(
episodeLengthInSeason,
seasonKey
- ) => (
-
-
- {$t(
- `series.${seriesSlug}`
- )}{' '}
- {$t('season', {
- s: seasonKey + 1,
- })}
-
- {_range(
- 1,
- episodeLengthInSeason +
- 1
- ).map(
- (
- chapterNum,
- chapterKey
- ) => {
- const currentSelection =
- series ===
- seriesKey &&
- season ===
- seasonKey +
- 1 &&
- chapter ===
- chapterNum
- return (
-
- )
+ ) => {
+ const season = seasonKey + 1
+ return (
+
- )
+ selected={
+ curSeries ===
+ seriesKey &&
+ curSeason === season
+ ? curChapter
+ : null
+ }
+ onClick={(chapter) => {
+ setQuery({
+ series: seriesKey,
+ s: season,
+ c: chapter,
+ })
+ }}
+ />
+ )
+ }
)}
)
- )}
-
-
- {Object.entries(
- StoriesData.Special?.[1] ?? []
- ).map(([chapterId, chapterItem], key) => {
- const seriesKey = Series.indexOf('Special')
- const chapterNum = Number(chapterId)
- const currentSelection =
- series === seriesKey &&
- season === 1 &&
- chapter === chapterNum
+ })}
+
+
+
{$t('Others')}
+ {(special ?? []).map((item, key) => {
return (
)
})}
@@ -174,17 +126,13 @@ const StoriesPage = () => {
- {seriesName === 'Special' ? (
-
+ {curSeries === SPECIAL_SERIES_TAG ? (
+
) : (
)}
@@ -193,6 +141,35 @@ const StoriesPage = () => {
)
}
-export const getStaticProps = getI18nProps(['stories'])
+export const getServerSideProps = async ({ locale }: { locale: string }) => {
+ const completion = (() => {
+ const ret: Partial
> = {}
+ for (let i = 0; i < Series.length; i++) {
+ const seriesSlug = Series[i]
+ ret[seriesSlug] = [
+ // skip index 0
+ [],
+ ...Episodes[seriesSlug].map((length, episodeKey) =>
+ _range(1, length + 1).map((chapterId) =>
+ storiesData?.[locale]?.data?.[
+ seriesSlug as SeriesName
+ ]?.[episodeKey + 1]?.[chapterId]
+ ? 1
+ : 0
+ )
+ ),
+ ]
+ }
+ return ret
+ })()
+
+ return {
+ props: {
+ ...(await addI18nMessages(locale, ['stories'])),
+ completion,
+ special: getSpecialStories(locale),
+ },
+ }
+}
-export default StoriesPage
+export default withQueryParam(StoriesPage)
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 0763d270..cfc266b7 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -49,6 +49,7 @@ specifiers:
mocha: ^10.0.0
next: 12.3.0
next-intl: ^2.7.5
+ next-query-params: ^4.0.0
nextjs-progressbar: ^0.0.14
postcss: ^8.4.16
prettier: ^2.7.1
@@ -62,6 +63,7 @@ specifiers:
tsconfig-paths: ^4.1.0
tslib: ^2.4.0
typescript: 4.8.3
+ use-query-params: ^2.1.1
wtf_wikipedia: ^10.0.2
dependencies:
@@ -94,6 +96,7 @@ dependencies:
lodash: 4.17.21
next: 12.3.0_vyk55zyolcaieh7zfm7nf7uva4
next-intl: 2.7.5_next@12.3.0+react@18.2.0
+ next-query-params: 4.0.0_rv7fcvaezkynccyly6h7ncwdb4
nextjs-progressbar: 0.0.14_next@12.3.0+react@18.2.0
postcss: 8.4.16
react: 18.2.0
@@ -102,6 +105,7 @@ dependencies:
rfdc: 1.3.0
screenfull: 6.0.2
tailwindcss: 3.1.8_57znarxsqwmnneadci5z5fd5gu
+ use-query-params: 2.1.1_biqbaboplfbrettd7655fr4n2y
devDependencies:
'@next/bundle-analyzer': 12.3.0
@@ -6264,6 +6268,19 @@ packages:
use-intl: 2.7.5_react@18.2.0
dev: false
+ /next-query-params/4.0.0_rv7fcvaezkynccyly6h7ncwdb4:
+ resolution: {integrity: sha512-u4PV4fA1Nb6CFZYnW+0s01YbHMJVhCmmnIZ11Dvma4rn8wUdjPPMuMlv2VMWziHD8DWE2+z3Y0475HfjdcT4MQ==}
+ peerDependencies:
+ next: ^10.0.0 || ^11.0.0 || ^12.0.0
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0
+ use-query-params: ^2.0.0
+ dependencies:
+ next: 12.3.0_vyk55zyolcaieh7zfm7nf7uva4
+ react: 18.2.0
+ tslib: 2.4.0
+ use-query-params: 2.1.1_biqbaboplfbrettd7655fr4n2y
+ dev: false
+
/next-tick/1.1.0:
resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==}
dev: true
@@ -7199,6 +7216,10 @@ packages:
randombytes: 2.1.0
dev: true
+ /serialize-query-params/2.0.1:
+ resolution: {integrity: sha512-MCw3M1sc0N0vTxsXfInqogI7Cygsnlv4Vdy1Sc+QAN50bpteYCIQRRS3FXT/mcCKOLxCR8ohLg29WmeOEXyjmw==}
+ dev: false
+
/set-blocking/2.0.0:
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
dev: false
@@ -7926,6 +7947,17 @@ packages:
use-isomorphic-layout-effect: 1.1.2_7v64pk2mkrohwh22gx7lrz5ive
dev: false
+ /use-query-params/2.1.1_biqbaboplfbrettd7655fr4n2y:
+ resolution: {integrity: sha512-6trNvHhbb9PjtNxIFA0l31A09WmafRrmcGtSPnqwKgoI7i+m741Oh3gjz9SkJFhE5FFruBWtXx7QyfmjGcI0jA==}
+ peerDependencies:
+ react: '>=16.8.0'
+ react-dom: '>=16.8.0'
+ dependencies:
+ react: 18.2.0
+ react-dom: 18.2.0_react@18.2.0
+ serialize-query-params: 2.0.1
+ dev: false
+
/use-sync-external-store/1.2.0_react@18.2.0:
resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==}
peerDependencies:
diff --git a/utils/useFrontendApi.tsx b/utils/useFrontendApi.tsx
index ec8c6611..fbb16f14 100644
--- a/utils/useFrontendApi.tsx
+++ b/utils/useFrontendApi.tsx
@@ -28,11 +28,15 @@ export type FrontendAPIResponseMapping = {
stories: Stories | null
}
| undefined
- eventStories: ChapterItem | null
+ 'characters/profile': {
+ profile: string
+ }
contributors: Contributor[]
+ eventStories: ChapterItem | null
diary: DiaryItem | undefined
news: { title: string; link?: string }[]
skillRunner: SkillLaunchItem[]
+ stories: ChapterItem | null
version:
| {
releaseDate: string
diff --git a/utils/withQueryParam.tsx b/utils/withQueryParam.tsx
new file mode 100644
index 00000000..22c37dea
--- /dev/null
+++ b/utils/withQueryParam.tsx
@@ -0,0 +1,16 @@
+import { QueryParamProvider } from 'use-query-params'
+import { NextAdapter } from 'next-query-params'
+
+type Component = (args: T) => JSX.Element
+
+function withQueryParam(Chlid: Component): Component {
+ return function QueryParamWrapped(props: T) {
+ return (
+
+
+
+ )
+ }
+}
+
+export default withQueryParam