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:
9
apps/directory/src/component/border.jsx
Normal file
9
apps/directory/src/component/border.jsx
Normal 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,
|
||||
}
|
||||
28
apps/directory/src/component/char_icon.jsx
Normal file
28
apps/directory/src/component/char_icon.jsx
Normal 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,
|
||||
}
|
||||
99
apps/directory/src/component/dropdown.jsx
Normal file
99
apps/directory/src/component/dropdown.jsx
Normal 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,
|
||||
}
|
||||
48
apps/directory/src/component/popup.jsx
Normal file
48
apps/directory/src/component/popup.jsx
Normal 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,
|
||||
}
|
||||
38
apps/directory/src/component/return_button.jsx
Normal file
38
apps/directory/src/component/return_button.jsx
Normal 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,
|
||||
}
|
||||
30
apps/directory/src/component/scss/border.module.scss
Normal file
30
apps/directory/src/component/scss/border.module.scss
Normal 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%;
|
||||
}
|
||||
}
|
||||
187
apps/directory/src/component/scss/dropdown.module.scss
Normal file
187
apps/directory/src/component/scss/dropdown.module.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
96
apps/directory/src/component/scss/popup.module.scss
Normal file
96
apps/directory/src/component/scss/popup.module.scss
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
55
apps/directory/src/component/scss/return_button.module.scss
Normal file
55
apps/directory/src/component/scss/return_button.module.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
78
apps/directory/src/component/scss/search_box.module.scss
Normal file
78
apps/directory/src/component/scss/search_box.module.scss
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
64
apps/directory/src/component/scss/switch.module.scss
Normal file
64
apps/directory/src/component/scss/switch.module.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
43
apps/directory/src/component/scss/totop_button.module.scss
Normal file
43
apps/directory/src/component/scss/totop_button.module.scss
Normal 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);
|
||||
}
|
||||
}
|
||||
50
apps/directory/src/component/search_box.jsx
Normal file
50
apps/directory/src/component/search_box.jsx
Normal 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,
|
||||
}
|
||||
35
apps/directory/src/component/switch.jsx
Normal file
35
apps/directory/src/component/switch.jsx
Normal 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,
|
||||
}
|
||||
64
apps/directory/src/component/totop_button.jsx
Normal file
64
apps/directory/src/component/totop_button.jsx
Normal 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,
|
||||
}
|
||||
46
apps/directory/src/component/voice.jsx
Normal file
46
apps/directory/src/component/voice.jsx
Normal 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,
|
||||
}
|
||||
Reference in New Issue
Block a user