feat: migrate to turbo (#22)

* feat: migrate top turbo

* ci: ci test

* fix: fix codeql issues

* feat: ci test

* chore: lint

* chore: misc changes

* feat: rename vite helpers

* feat: use fetch to handle assets

* feat: update directory

* feat: fetch charword table

* feat: migrate download game data and detect missing voice files

* feat: symlink relative path

* feat: finish wrangler upload

* feat: migrate wrangler download

* feat: finish

* chore: auto update

* ci: update ci

* ci: update ci

---------

Co-authored-by: Halyul <Halyul@users.noreply.github.com>
This commit is contained in:
Haoyu Xu
2025-02-22 15:11:30 +08:00
committed by GitHub
parent 17c61ce5d4
commit d6e7bc20d3
352 changed files with 12911 additions and 9411 deletions

View File

@@ -0,0 +1,9 @@
import PropTypes from 'prop-types'
import classes from './scss/border.module.scss'
export default function Border(props) {
return <section className={classes.border}>{props.children}</section>
}
Border.propTypes = {
children: PropTypes.node,
}

View File

@@ -0,0 +1,28 @@
import PropTypes from 'prop-types'
export default function CharIcon(props) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox={props.viewBox}
style={props.style}
>
{props.type === 'operator' ? (
<g>
<path d="M89 17.5 30.4 57 24.3 71.4 82.9 32.6Z"></path>
<path d="M0 17.5 58.6 57 64.7 71.4 6.1 32.7Z"> </path>
<path d="M89 0 30.4 39.5 24.3 53.9 82.9 15.1Z"> </path>
<path d="M0 0 58.6 39.5 64.7 53.9 6.1 15.2Z"> </path>
</g>
) : (
<path d="M90.4 50.6l-39.8-23.5v-4c0-4.5-5-6.5-5-6.5a5.4 5.4 0 012.2-10.1c2.7 0 5.3 1.5 5.5 4.8.4 5.3 6.4 3.9 6.4-.3a11.7 11.7 0 00-12-11c-9 0-11.6 8.8-11.6 11.6a11.5 11.5 0 001.6 6.2c2.2 3.8 6.6 4.3 6.6 6.8v2.5L4.2 50.7c-4 2.3-4.7 7.3-3.8 10.3a9.1 9.1 0 009.1 6.4h75.2c5.9 0 8.6-3.4 9.5-6.3C95 58.1 95 53.4 90.4 50.6Zm-5.6 10.3h-75.2c-2.4.1-4-3.3-1.5-4.8l39.2-22.9 39 22.8A2.7 2.7 0 0184.7 60.8Z" />
)}
</svg>
)
}
CharIcon.propTypes = {
viewBox: PropTypes.string,
type: PropTypes.string,
style: PropTypes.object,
}

View File

@@ -0,0 +1,99 @@
import { useState } from 'react'
import PropTypes from 'prop-types'
import classes from './scss/dropdown.module.scss'
export default function Dropdown(props) {
const [hidden, setHidden] = useState(true)
const toggleDropdown = () => {
setHidden(!hidden)
}
return (
<>
<section
className={`${classes.dropdown} ${hidden ? '' : classes.active} ${props.className ? props.className : ''} ${props.left ? classes.left : ''}`}
>
<section
className={classes.text}
onClick={() => toggleDropdown()}
>
<span className={classes.content}>{props.text}</span>
<span
className={classes.icon}
style={props.iconStyle}
></span>
<section className={classes.popup}>
<span className={classes.text}>{props.altText}</span>
</section>
</section>
<ul className={classes.menu} style={props.activeColor}>
{props.menu.map((item) => {
switch (item.type) {
case 'date': {
return (
<section
key={item.name}
className={classes.date}
>
<section className={classes.line} />
<section className={classes.text}>
{item.name}
</section>
</section>
)
}
case 'custom': {
return item.component
}
default: {
return (
<li
key={item.name}
className={`${classes.item} ${item.name === props.text || (props.activeRule && props.activeRule(item)) ? classes.active : ''}`}
onClick={() => {
props.onClick(item)
toggleDropdown()
}}
style={
item.color
? { color: item.color }
: {}
}
>
{item.icon ? (
<section
className={classes['item-icon']}
>
{item.icon}
</section>
) : null}
<section className={classes.text}>
{item.name}
</section>
</li>
)
}
}
})}
</ul>
<section
className={classes.overlay}
hidden={hidden}
onClick={() => toggleDropdown()}
/>
</section>
</>
)
}
Dropdown.propTypes = {
className: PropTypes.string,
text: PropTypes.string,
menu: PropTypes.array,
onClick: PropTypes.func,
activeColor: PropTypes.object,
activeRule: PropTypes.func,
altText: PropTypes.string,
iconStyle: PropTypes.object,
left: PropTypes.bool,
}

View File

@@ -0,0 +1,48 @@
import { useState } from 'react'
import classes from './scss/popup.module.scss'
import ReturnButton from '@/component/return_button'
import Border from '@/component/border'
import PropTypes from 'prop-types'
export default function Popup(props) {
const [hidden, setHidden] = useState(true)
const toggle = () => {
setHidden(!hidden)
}
return (
<>
<section
className={`${classes.popup} ${hidden ? '' : classes.active}`}
>
<section className={classes.wrapper}>
<section className={classes.title}>
<section className={classes.text}>
{props.title}
</section>
<ReturnButton
onClick={toggle}
className={classes['return-button']}
/>
</section>
<Border />
<section className={classes.content}>
{props.children}
</section>
</section>
<section
className={`${classes.overlay} ${hidden ? '' : classes.active}`}
onClick={() => toggle()}
/>
</section>
<span className={classes['entry-text']} onClick={toggle}>
{props.title}
</span>
</>
)
}
Popup.propTypes = {
title: PropTypes.string,
children: PropTypes.node,
}

View File

@@ -0,0 +1,38 @@
import PropTypes from 'prop-types'
import classes from './scss/return_button.module.scss'
export default function ReturnButton(props) {
return (
<>
<section
className={`${classes['return-button']} ${props.className ? props.className : ''}`}
onClick={() => props.onClick()}
>
<section className={classes.wrapper}>
<section className={classes['arrow-left']}></section>
<section className={classes.bar}></section>
<section className={classes['arrow-right']}></section>
</section>
<section className={classes.wrapper}>
<section className={classes['arrow-left']}></section>
<section className={classes.bar}></section>
<section className={classes['arrow-right']}></section>
</section>
<section className={classes.wrapper}>
<section className={classes['arrow-left']}></section>
<section className={classes.bar}></section>
<section className={classes['arrow-right']}></section>
</section>
<section className={classes.wrapper}>
<section className={classes['arrow-left']}></section>
<section className={classes.bar}></section>
<section className={classes['arrow-right']}></section>
</section>
</section>
</>
)
}
ReturnButton.propTypes = {
onClick: PropTypes.func,
className: PropTypes.string,
}

View File

@@ -0,0 +1,30 @@
.border {
position: relative;
bottom: 1px;
border-bottom: 1px solid var(--text-color);
@media only screen and (width <= 430px) {
& {
margin: 0 1rem;
}
}
&::before,
&::after {
content: '';
display: block;
position: absolute;
width: 5px;
height: 5px;
top: -2px;
background-color: var(--text-color);
}
&::before {
right: 100%;
}
&::after {
left: 100%;
}
}

View File

@@ -0,0 +1,187 @@
.dropdown {
position: relative;
display: inline-block;
user-select: none;
z-index: 2;
padding: 0.5em;
cursor: pointer;
.text {
display: flex;
flex-direction: row;
align-items: center;
color: var(--text-color);
height: 2em;
min-width: 2em;
.popup {
opacity: 0;
position: absolute;
background-color: var(--root-background-color);
width: max-content;
height: max-content;
max-height: 61.8vh;
max-width: 61.8vw;
z-index: -1;
top: 2.5em;
right: 0;
cursor: auto;
transition: all cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
overflow: auto;
padding: 0.5rem;
border: 1px solid var(--border-color);
visibility: hidden;
.text {
font-size: 1rem;
}
}
&:hover {
.popup {
visibility: unset;
opacity: 1;
}
}
}
.content {
padding-right: 1.2em;
height: 1em;
}
.icon {
position: absolute;
bottom: 0.5em;
right: 0.6em;
width: 0.5em;
height: 0.5em;
display: inline-block;
vertical-align: middle;
border-left: 0.15em solid var(--text-color);
border-bottom: 0.15em solid var(--text-color);
border-right: 0.15em solid var(--text-color);
border-top: 0.15em solid var(--text-color);
transform: translateY(-0.7em) rotate(-45deg);
}
.menu {
scrollbar-gutter: stable;
opacity: 0;
position: absolute;
background-color: var(--root-background-color);
width: max-content;
max-height: 61.8vh;
max-width: 61.8vw;
z-index: -1;
top: 2.5em;
right: 0;
display: flex;
align-items: stretch;
flex-flow: column nowrap;
transition: all cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
overflow: auto;
padding: 0.5rem;
border: 1px solid var(--border-color);
visibility: hidden;
color: var(--link-highlight-color);
cursor: auto;
&.left {
left: 0;
right: unset;
}
.date {
font-family:
Bender, 'Noto Sans SC', 'Noto Sans JP', 'Noto Sans KR',
'Noto Sans', sans-serif;
font-weight: bold;
font-size: 1.5rem;
display: flex;
flex-flow: row nowrap;
align-items: center;
justify-content: space-between;
width: 100%;
.line {
height: 1px;
flex-grow: 1;
background-color: var(--text-color);
margin: 0.5rem;
}
}
.item {
cursor: pointer;
padding: 0.5rem;
font-size: 1rem;
display: flex;
justify-content: space-between;
align-items: center;
.text {
flex: 1;
transition: color cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
margin-left: 1rem;
}
.item-icon svg {
transition: fill cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
width: 1rem;
fill: var(--text-color);
}
&:hover,
&:focus,
&.active {
.text {
color: currentcolor;
}
.item-icon svg {
fill: currentcolor;
}
}
}
}
&.left {
/* stylelint-disable no-descending-specificity */
.popup,
.menu {
left: 0;
right: unset;
}
}
.overlay {
z-index: -1;
position: fixed;
inset: 0;
width: 100vw;
height: 100vh;
cursor: auto;
}
&.active,
&:hover {
.icon {
animation: icon-flash 2s cubic-bezier(0.65, 0.05, 0.36, 1) infinite;
}
}
&.active {
.menu {
visibility: visible;
opacity: 1;
z-index: 2;
}
}
}
@keyframes icon-flash {
50% {
opacity: 0.2;
}
}

View File

@@ -0,0 +1,96 @@
.entry-text {
cursor: pointer;
}
.popup {
position: fixed;
inset: 0;
overflow: hidden;
opacity: 0;
z-index: -1;
border: unset;
transition: all cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: 1.2rem;
.wrapper {
display: flex;
flex-flow: column nowrap;
align-items: stretch;
max-width: 480px;
height: fit-content;
margin: 0 auto;
background-color: var(--root-background-color);
border: 1px solid var(--border-color);
padding: 2rem;
}
.title {
font-size: 3rem;
font-weight: 700;
display: flex;
flex-direction: row;
place-content: center space-between;
align-items: center;
text-transform: uppercase;
font-family:
Geometos, 'Noto Sans SC', 'Noto Sans JP', 'Noto Sans KR',
'Noto Sans', sans-serif;
.return-button {
color: var(--button-color);
transition: color cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
&:hover {
color: var(--text-color);
}
}
}
.text {
flex-grow: 1;
margin-right: 3rem;
}
.content {
line-height: 1.3em;
padding: 1rem 1rem 0;
user-select: text;
}
.overlay {
position: absolute;
inset: 0;
z-index: -1;
opacity: 0;
background-color: var(--root-background-color);
transition: all cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
&.active {
opacity: 0.5;
visibility: visible;
}
}
&.active {
opacity: 1;
z-index: 10;
}
@media (width <= 768px) {
.title {
font-size: 2rem;
}
.content {
font-size: 1rem;
}
.return-button {
transform: scale(0.8);
}
}
}

View File

@@ -0,0 +1,55 @@
.return-button {
display: flex;
flex-direction: column;
align-items: flex-start;
padding: 0.6rem 0;
width: 3rem;
cursor: pointer;
%arrow-shared {
border-top: 0.24rem solid transparent;
border-bottom: 0.24rem solid transparent;
}
.wrapper {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
&:nth-child(1) {
transform: translateY(-0.1rem) rotate(45deg);
}
&:nth-child(2) {
transform: translateY(-0.1rem) translate(90%, -100%) rotate(-45deg);
}
&:nth-child(3) {
transform: translateY(-0.1rem) translateY(150%) rotate(315deg);
}
&:nth-child(4) {
transform: translateY(-0.1rem) translate(90%, 50%) rotate(225deg);
}
}
.bar {
width: 1rem;
height: 0.4rem;
background-color: currentcolor;
transition: transform cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
}
.arrow-left {
@extend %arrow-shared;
border-right: 0.3rem solid currentcolor;
}
.arrow-right {
@extend %arrow-shared;
border-left: 0.3rem solid currentcolor;
}
}

View File

@@ -0,0 +1,78 @@
.search-box {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: stretch;
margin: 0.5rem;
.icon {
position: absolute;
width: 0.8rem;
height: 0.8rem;
display: inline-block;
vertical-align: middle;
border-left: 0.15rem solid var(--text-color-full);
border-bottom: 0.15rem solid var(--text-color-full);
border-right: 0.15rem solid var(--text-color-full);
border-top: 0.15rem solid var(--text-color-full);
transform: translate(0.2rem, 0.3rem) rotate(-45deg);
}
.icon-dot {
position: absolute;
background-color: var(--text-color-full);
width: 0.15rem;
height: 0.6rem;
transform: translate(1.2rem, 1.1rem) rotate(-45deg);
}
.input {
flex-grow: 1;
font-size: 1.5rem;
width: 100%;
margin-left: 2rem;
padding-right: 2rem;
background-color: transparent;
border: unset;
border-bottom: 0.15em solid var(--home-item-outline-color);
color: var(--text-color);
transition: all cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
}
.input:focus,
.input:hover {
outline: none;
border-bottom: 0.15em solid var(--text-color);
}
.icon-clear {
position: absolute;
right: 1rem;
width: 2rem;
height: 2rem;
cursor: pointer;
opacity: 0;
transition: all cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
visibility: hidden;
&.active {
opacity: 1;
visibility: unset;
}
.line {
position: absolute;
width: 2rem;
height: 0.2rem;
background-color: var(--text-color);
&:nth-child(1) {
transform: translate(0, 0.8rem) rotate(45deg);
}
&:nth-child(2) {
transform: translate(0, 0.8rem) rotate(-45deg);
}
}
}
}

View File

@@ -0,0 +1,64 @@
.switch {
position: relative;
user-select: none;
z-index: 2;
padding: 8px 36px 8px 8px;
cursor: pointer;
display: flex;
flex-direction: row;
align-items: center;
color: var(--secondary-text-color);
transition: color cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
.content {
padding-right: 8px;
}
.wrapper {
color: var(--secondary-text-color);
transition: all cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
.icon {
position: absolute;
bottom: 0.65em;
right: 18px;
width: 0.5em;
height: 0.5em;
display: inline-block;
vertical-align: middle;
border-left: 0.15em solid currentcolor;
border-bottom: 0.15em solid currentcolor;
border-right: 0.15em solid currentcolor;
border-top: 0.15em solid currentcolor;
transform: translate(0, -0.15em) rotate(-45deg);
transition:
right cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s,
background-color cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
}
.line {
position: absolute;
bottom: 1.1em;
right: 6px;
width: 18px;
height: 0.15em;
display: inline-block;
vertical-align: middle;
background-color: currentcolor;
z-index: -1;
}
}
&.active {
color: var(--text-color);
.wrapper {
color: var(--text-color-full);
.icon {
background-color: currentcolor;
right: 0;
}
}
}
}

View File

@@ -0,0 +1,43 @@
.totop-button {
position: fixed;
user-select: none;
z-index: 2;
cursor: pointer;
right: 2rem;
bottom: 1rem;
display: flex;
flex-direction: column;
align-items: stretch;
justify-content: flex-start;
width: 3rem;
height: 3rem;
opacity: 0;
visibility: hidden;
transition: opacity cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
&.show {
opacity: 1;
visibility: unset;
}
.bar {
position: absolute;
width: 100%;
height: 0.3rem;
background-color: var(--text-color-full);
will-change: auto;
}
.bar:nth-child(1) {
transform: rotateZ(45deg) scaleX(0.5) translateX(45%);
}
.bar:nth-child(2) {
transform: rotateZ(-45deg) scaleX(0.5) translateX(-45%);
}
.bar:nth-child(3),
.bar:nth-child(4) {
transform: translateY(450%) rotateZ(90deg) scaleX(0.5);
}
}

View File

@@ -0,0 +1,50 @@
import { useState } from 'react'
import PropTypes from 'prop-types'
import classes from './scss/search_box.module.scss'
import { useI18n } from '@/state/language'
export default function SearchBox(props) {
const { i18n } = useI18n()
const [searchField, setSearchField] = useState('')
const filterBySearch = (event) => {
const query = event.target.value
props.handleOnChange(query)
setSearchField(query)
}
return (
<>
<section
className={`${classes['search-box']} ${props.className ? props.className : ''}`}
>
<section className={classes.icon} />
<section className={classes['icon-dot']} />
<input
type="text"
className={classes.input}
placeholder={i18n(props.altText)}
onChange={filterBySearch}
value={searchField}
/>
<section
className={`${classes['icon-clear']} ${searchField === '' ? '' : classes.active}`}
onClick={() => {
setSearchField('')
props.handleOnChange('')
}}
>
<section className={classes.line} />
<section className={classes.line} />
</section>
</section>
</>
)
}
SearchBox.propTypes = {
className: PropTypes.string,
text: PropTypes.string,
altText: PropTypes.string,
handleOnChange: PropTypes.func,
searchField: PropTypes.string,
}

View File

@@ -0,0 +1,35 @@
import { useState, useEffect } from 'react'
import PropTypes from 'prop-types'
import classes from './scss/switch.module.scss'
import { useI18n } from '@/state/language'
export default function Switch(props) {
const [on, setOn] = useState(props.on)
const { i18n } = useI18n()
useEffect(() => {
setOn(props.on)
}, [props.on])
return (
<section
className={`${classes.switch} ${on ? classes.active : ''}`}
onClick={() => {
if (props.handleOnClick) {
props.handleOnClick(!on)
}
}}
>
<span className={classes.text}>{i18n(props.text)}</span>
<section className={classes.wrapper}>
<span className={classes.line}></span>
<span className={classes.icon}></span>
</section>
</section>
)
}
Switch.propTypes = {
on: PropTypes.bool,
text: PropTypes.string,
handleOnClick: PropTypes.func,
}

View File

@@ -0,0 +1,64 @@
import { useEffect, useState, useCallback } from 'react'
import PropTypes from 'prop-types'
import classes from './scss/totop_button.module.scss'
export default function ToTopButton(props) {
const [hidden, setHidden] = useState(true)
useEffect(() => {
const handleButton = () => {
const scrollBarPos = window.scrollY || 0
setHidden(!(scrollBarPos > 100))
}
window.addEventListener('scroll', handleButton)
return () => {
window.removeEventListener('scroll', handleButton)
}
}, [])
const smoothScroll = useCallback((target) => {
const targetElement = document.querySelector(target)
const targetPosition =
targetElement.getBoundingClientRect().top + window.scrollY
const startPosition = window.scrollY
const distance = targetPosition - startPosition
const duration = 1000
let start = null
window.requestAnimationFrame(step)
function step(timestamp) {
if (!start) start = timestamp
const progress = timestamp - start
window.scrollTo(
0,
easeInOutCubic(progress, startPosition, distance, duration)
)
if (progress < duration) window.requestAnimationFrame(step)
}
function easeInOutCubic(t, b, c, d) {
t /= d / 2
if (t < 1) return (c / 2) * t * t * t + b
t -= 2
return (c / 2) * (t * t * t + 2) + b
}
}, [])
return (
<>
<section
className={`${classes['totop-button']} ${hidden ? '' : classes.show} ${props.className ? props.className : ''}`}
onClick={() => {
smoothScroll('#root')
}}
>
<section className={classes.bar}></section>
<section className={classes.bar}></section>
<section className={classes.bar}></section>
<section className={classes.bar}></section>
</section>
</>
)
}
ToTopButton.propTypes = {
onClick: PropTypes.func,
className: PropTypes.string,
}

View File

@@ -0,0 +1,46 @@
import { useEffect, useRef } from 'react'
import PropTypes from 'prop-types'
export default function VoiceElement({ src, replay, handleAduioStateChange }) {
const audioRef = useRef(null)
useEffect(() => {
if (src) {
audioRef.current.src = src
audioRef.current.play()
} else {
audioRef.current.pause()
}
}, [src])
useEffect(() => {
if (replay) {
audioRef.current.currentTime = 0
audioRef.current.play()
}
}, [replay])
return (
<audio
ref={audioRef}
preload="auto"
autoPlay
onEnded={(e) => {
if (handleAduioStateChange) handleAduioStateChange(e, 'ended')
}}
onPlay={(e) => {
if (handleAduioStateChange) handleAduioStateChange(e, 'play')
}}
onPause={(e) => {
if (handleAduioStateChange) handleAduioStateChange(e, 'pause')
}}
>
<source type="audio/ogg" />
</audio>
)
}
VoiceElement.propTypes = {
src: PropTypes.string,
handleAduioStateChange: PropTypes.func,
replay: PropTypes.bool,
}