Skip to content

Commit 3d5484e

Browse files
committed
More char stuff
1 parent 7b9b4df commit 3d5484e

5 files changed

Lines changed: 314 additions & 38 deletions

File tree

components/YouTube.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export default function YouTube({ vidID }: { vidID: string }) {
1+
export default function YouTube({ vidID, autoplay }: { vidID: string, autoplay?: true }) {
22
return <div style={{
33
position: "relative",
44
paddingBottom: "56.25%" /* 16:9 */,
@@ -13,7 +13,7 @@ export default function YouTube({ vidID }: { vidID: string }) {
1313
width: "100%",
1414
height: "100%"
1515
}}
16-
src={`https://www.youtube.com/embed/${vidID}`}
16+
src={`https://www.youtube.com/embed/${vidID}${autoplay ? "?autoplay=1" : ""}`}
1717
frameBorder="0"
1818
/>
1919
</div>

pages/characters/[name].tsx

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
import { GetStaticPathsResult, GetStaticPropsContext, GetStaticPropsResult } from "next"
2+
import Head from "next/head"
3+
import Image from "next/image"
4+
import { useState } from "react"
5+
import FormattedLink from "../../components/FormattedLink"
6+
import Main from "../../components/Main"
7+
import YouTube from "../../components/YouTube"
8+
import { getCharacterCurves, getCharacters, urlify } from "../../utils/data-cache"
9+
import { elements, ElementType, getCharStatsAt, isFullCharacter, stat, weapons } from "../../utils/utils"
10+
import { Character, CharacterFull, CostTemplate, CurveEnum, Skills } from "../../utils/types"
11+
12+
interface Props {
13+
char: Character,
14+
characterCurves: Record<CurveEnum, number[]> | null
15+
}
16+
17+
export default function CharacterWebpage({ char, location, characterCurves }: Props & { location: string }) {
18+
const charElems = char.skills?.map(skill => skill.ult?.type).filter(x => x) as ElementType[] ?? [char.meta.element]
19+
return (
20+
<Main>
21+
<Head>
22+
<title>{char.name} | Hu Tao</title>
23+
<meta name="twitter:card" content="summary" />
24+
<meta property="og:title" content={`${char.name} | Hu Tao`} />
25+
<meta property="og:description" content={`View ${char.name} information`} />
26+
</Head>
27+
<h2 className="font-semibold">
28+
<FormattedLink href="/characters/" location={location} font="semibold" size="lg">
29+
Characters
30+
</FormattedLink>
31+
</h2>
32+
33+
<h1 className="text-4xl font-bold pb-2">
34+
{char.name}
35+
</h1>
36+
37+
<div className="float-right border-2 border-slate-500 dark:bg-slate-600 bg-slate-200 m-2">
38+
Table of Content
39+
</div>
40+
41+
<div>
42+
<blockquote className="pl-5 border-l-2">
43+
{char.desc}
44+
</blockquote>
45+
46+
{charElems.map(e => <div key={e} className="w-5 inline-block pr-1">
47+
<Image src={elements[e]} alt={`${e} Element`} />
48+
</div>)}
49+
50+
{char.star && <div className="inline-block pr-2">
51+
{char.star}
52+
</div>}
53+
54+
{char.weaponType ? <div className="inline-block">
55+
<div className="w-5 inline-block pr-1">
56+
<Image src={weapons[char.weaponType]} alt={`${char.weaponType} Element`} />
57+
</div>
58+
59+
{char.weaponType} user
60+
</div> : <div className="inline-block">Character (unreleased)</div>}
61+
62+
{char.ascensionCosts && <AscensionCosts costs={char.ascensionCosts} />}
63+
{char.skills && <TalentCosts skills={char.skills} />}
64+
{isFullCharacter(char) && characterCurves && <QuickStats char={char} curves={characterCurves}/>}
65+
{char.media.videos && <Videos videos={char.media.videos} />}
66+
</div>
67+
68+
</Main>
69+
)
70+
}
71+
72+
function AscensionCosts({ costs }: { costs: CostTemplate }) {
73+
const ascensionCosts = [
74+
costs.mapping.Gem4,
75+
costs.mapping.BossMat,
76+
costs.mapping.Specialty,
77+
costs.mapping.EnemyDropTier3,
78+
].filter(x => x)
79+
return <div>
80+
<h4 className="text-base font-semibold pt-1 inline-block pr-1">Ascension materials:</h4>
81+
{ascensionCosts.map(e => <div className="inline-block pr-1" key={e}>
82+
{e}
83+
</div>)}
84+
</div>
85+
}
86+
87+
function TalentCosts({ skills }: { skills: Skills[] }) {
88+
const talents = skills
89+
.flatMap(s => [...(s.talents ?? []), s.ult])
90+
.filter(x => x)
91+
92+
const books = talents
93+
.flatMap(s => [
94+
s?.costs?.mapping.Book,
95+
s?.costs?.mapping.Book1,
96+
s?.costs?.mapping.Book2,
97+
s?.costs?.mapping.Book3,
98+
])
99+
.filter((x, i, a) => x && a.indexOf(x) == i)
100+
.map(x => `Guide to ${x}`)
101+
102+
const mats = talents
103+
.map(s => s?.costs?.mapping.BossMat)
104+
.filter((x, i, a) => x && a.indexOf(x) == i)
105+
106+
const drops = talents
107+
.map(s => s?.costs?.mapping.EnemyDropTier3)
108+
.filter((x, i, a) => x && a.indexOf(x) == i)
109+
110+
const all = [...books, ...mats, ...drops]
111+
return <div>
112+
<h4 className="text-base font-semibold pt-1 inline-block pr-1">Talent materials:</h4>
113+
{all.map(e => <div className="inline-block pr-1" key={e}>
114+
{e}
115+
</div>)}
116+
</div>
117+
}
118+
119+
function QuickStats({ char, curves }: { char: CharacterFull, curves: Record<CurveEnum, number[]> }) {
120+
const maxAscension = char.ascensions[char.ascensions.length - 1]
121+
122+
const base = getCharStatsAt(char, 1, 0, curves)
123+
const max = getCharStatsAt(char, maxAscension.maxLevel, maxAscension.level, curves)
124+
125+
// TODO
126+
127+
return <div className="flex flex-row justify-evenly">
128+
<div>
129+
<h4 className="text-base font-semibold pt-1 inline-block pr-1">Base stats:</h4>
130+
{Object.entries(base)
131+
.map(([name, value]) => <div className="pr-1" key={name}>
132+
{name}: {stat(name, value)}
133+
</div>)}
134+
</div>
135+
<div>
136+
<h4 className="text-base font-semibold pt-1 inline-block pr-1">Max stats:</h4>
137+
{Object.entries(max)
138+
.map(([name, value]) => <div className="pr-1" key={name}>
139+
{name}: {stat(name, value)}
140+
</div>)}
141+
</div>
142+
</div>
143+
}
144+
145+
function Videos({ videos }: { videos: Record<string, string> }) {
146+
const multiple = Object.keys(videos).length > 1
147+
148+
return <div>
149+
<h3 className="text-lg font-bold pt-1">{multiple ? "Videos" : "Video"}:</h3>
150+
{Object.entries(videos).map(([name, link]) => <Video key={name} name={name} link={link} />)}
151+
</div>
152+
}
153+
154+
function Video({ name, link }: { name: string, link: string }) {
155+
const [expanded, setExpanded] = useState(false)
156+
157+
return <div className={`${expanded ? "text-blue-600 dark:text-blue-400 hover:text-blue-400 dark:hover:text-blue-500" : "text-blue-700 dark:text-blue-300 hover:text-blue-400 dark:hover:text-blue-400"} cursor-pointer`} onClick={() => setExpanded(!expanded)}>
158+
{name}
159+
{expanded && <YouTube vidID={link.replace("https://youtu.be/", "")} autoplay />}
160+
</div>
161+
}
162+
163+
export async function getStaticProps(context: GetStaticPropsContext): Promise<GetStaticPropsResult<Props>> {
164+
const charName = context.params?.name
165+
const data = await getCharacters()
166+
167+
const char = Object.values(data ?? {}).find(g => urlify(g.name, false) == charName)
168+
if (!data || !char) {
169+
return {
170+
notFound: true,
171+
revalidate: 5 * 60
172+
}
173+
}
174+
175+
let characterCurves = null
176+
if (isFullCharacter(char)) {
177+
const curves = await getCharacterCurves()
178+
if (curves)
179+
characterCurves = Object.fromEntries(char.curves.map(c => c.curve).filter((v, i, arr) => arr.indexOf(v) == i).map(c => [c, curves[c]])) as Record<CurveEnum, number[]>
180+
}
181+
182+
return {
183+
props: {
184+
char,
185+
characterCurves
186+
},
187+
revalidate: 60 * 60
188+
}
189+
}
190+
191+
export async function getStaticPaths(): Promise<GetStaticPathsResult> {
192+
const data = await getCharacters()
193+
return {
194+
paths: Object.values(data ?? {}).map(g => ({
195+
params: { name: urlify(g.name, false) }
196+
})) ?? [],
197+
fallback: "blocking"
198+
}
199+
}

pages/characters/index.tsx

Lines changed: 12 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,31 +4,11 @@ import Image from "next/image"
44
import { Dispatch, SetStateAction, useState } from "react"
55
import FormattedLink from "../../components/FormattedLink"
66
import Main from "../../components/Main"
7-
import Anemo from "../../public/img/element/Anemo.png"
8-
import Cryo from "../../public/img/element/Cryo.png"
9-
import Dendro from "../../public/img/element/Dendro.png"
10-
import Electro from "../../public/img/element/Electro.png"
11-
import Geo from "../../public/img/element/Geo.png"
12-
import Hydro from "../../public/img/element/Hydro.png"
13-
import Pyro from "../../public/img/element/Pyro.png"
14-
import Bow from "../../public/img/weapon_types/Bow.png"
15-
import Catalyst from "../../public/img/weapon_types/Catalyst.png"
16-
import Claymore from "../../public/img/weapon_types/Claymore.png"
17-
import Polearm from "../../public/img/weapon_types/Polearm.png"
18-
import Sword from "../../public/img/weapon_types/Sword.png"
197
import { getCharacters, urlify } from "../../utils/data-cache"
20-
import { Character, CharacterFull, WeaponType } from "../../utils/types"
8+
import { WeaponType } from "../../utils/types"
9+
import { elements, ElementType, isFullCharacter, weapons } from "../../utils/utils"
2110

2211

23-
const elements = {
24-
Pyro, Electro, Cryo, Hydro, Anemo, Geo, Dendro
25-
}
26-
const weapons: Record<WeaponType, StaticImageData> = {
27-
Polearm, Sword, Claymore, Bow, Catalyst
28-
}
29-
30-
type ElementType = (keyof (typeof elements))
31-
3212
interface SmallChar {
3313
name: string
3414
stars?: number
@@ -78,12 +58,12 @@ export default function Characters(props: Props & { location: string }) {
7858
<ExclusiveButton type={starFilter} value={0} setter={setStarFilter}>
7959
All
8060
</ExclusiveButton>
81-
<ExclusiveButton type={starFilter} value={4} setter={setStarFilter}>
82-
4★ Only
83-
</ExclusiveButton>
8461
<ExclusiveButton type={starFilter} value={5} setter={setStarFilter}>
8562
5★ Only
8663
</ExclusiveButton>
64+
<ExclusiveButton type={starFilter} value={4} setter={setStarFilter}>
65+
4★ Only
66+
</ExclusiveButton>
8767
</div>
8868
</div>
8969

@@ -97,6 +77,9 @@ export default function Characters(props: Props & { location: string }) {
9777
<div className="flex flex-row flex-wrap gap-2 pt-2">
9878
{defaultElements.map(e => (
9979
<ToggleButton key={e} type={elementFilter} value={e} setter={setElementFilter}>
80+
<div className="w-4 inline-block pr-1">
81+
<Image src={elements[e]} alt={`${e} Element`} />
82+
</div>
10083
{e}
10184
</ToggleButton>
10285
))}
@@ -113,6 +96,9 @@ export default function Characters(props: Props & { location: string }) {
11396
<div className="flex flex-row flex-wrap gap-2 pt-2">
11497
{defaultWeapons.map(e => (
11598
<ToggleButton key={e} type={weaponFilter} value={e} setter={setWeaponFilter}>
99+
<div className="w-4 inline-block pr-1">
100+
<Image src={weapons[e]} alt={e} />
101+
</div>
116102
{e}
117103
</ToggleButton>
118104
))}
@@ -149,7 +135,7 @@ export default function Characters(props: Props & { location: string }) {
149135
<span className="absolute block p-0.5 top-0 w-full">
150136
<div className="flex flex-col float-right">
151137
{char.weapon && <div className="w-6 md:w-8">
152-
<Image src={weapons[char.weapon]} alt={`${char.weapon}`} />
138+
<Image src={weapons[char.weapon]} alt={char.weapon} />
153139
</div>}
154140
</div>
155141
</span>
@@ -208,10 +194,6 @@ function Icon({ char, className }: { char: SmallChar, className?: string }) {
208194
return <Image alt={char.name} src={src} className={className} width={256} height={256} onError={(e) => (e.target as HTMLImageElement).src = "/img/unknown.png"} />
209195
}
210196

211-
function isFullCharacter(char: Character): char is CharacterFull {
212-
return typeof (char as CharacterFull).releasedOn == "string"
213-
}
214-
215197
export async function getStaticProps(context: GetStaticPropsContext): Promise<GetStaticPropsResult<Props>> {
216198
const data = await getCharacters()
217199

utils/data-cache.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
1-
import { Character, Guide } from "./types"
1+
import { Character, CurveEnum, Guide } from "./types"
22

33
const baseURL = "https://raw.githubusercontent.com/Tibowl/HuTao/master/src/data"
44

55
type Guides = Guide[]
66
type Characters = Record<string, Character>
7+
type CharacterCurves = Record<CurveEnum, number[]>
8+
type CharacterLevels = number[]
79

810
type Cache = {
911
guides: Cacher<Guides>
10-
character: Cacher<Characters>
12+
characters: Cacher<Characters>
13+
characterCurves: Cacher<CharacterCurves>
14+
characterLevels: Cacher<CharacterLevels>
1115
}
1216

1317
interface Cacher<T> {
@@ -19,9 +23,15 @@ const cached: Cache = {
1923
guides: {
2024
time: 0
2125
},
22-
character: {
23-
time: 0
24-
}
26+
characters: {
27+
time: 0
28+
},
29+
characterCurves: {
30+
time: 0
31+
},
32+
characterLevels: {
33+
time: 0
34+
}
2535
}
2636
export function urlify(input: string, shouldYeetBrackets: boolean): string {
2737
if (shouldYeetBrackets)
@@ -34,7 +44,9 @@ export function yeetBrackets(input: string) {
3444
}
3545

3646
export const getGuides: (() => Promise<Guides | undefined>) = createGetCacheable("guides")
37-
export const getCharacters: (() => Promise<Characters | undefined>) = createGetCacheable("character", "gamedata/characters")
47+
export const getCharacters: (() => Promise<Characters | undefined>) = createGetCacheable("characters", "gamedata/characters")
48+
export const getCharacterCurves: (() => Promise<CharacterCurves | undefined>) = createGetCacheable("characterCurves", "gamedata/character_curves")
49+
export const getCharacterLevels: (() => Promise<CharacterLevels | undefined>) = createGetCacheable("characterLevels", "gamedata/character_levels")
3850

3951

4052
function createGetCacheable(name: keyof Cache, path: string = name): () => Promise<any> {

0 commit comments

Comments
 (0)