Merge pull request #47 from jedmund/frontend-refactor
Frontend refactor
This commit is contained in:
commit
f3255e1381
158 changed files with 9181 additions and 6811 deletions
1
.prettierignore
Normal file
1
.prettierignore
Normal file
|
|
@ -0,0 +1 @@
|
|||
utils/api.tsx
|
||||
5
.prettierrc
Normal file
5
.prettierrc
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"semi": false,
|
||||
"tabWidth": 2,
|
||||
"singleQuote": true
|
||||
}
|
||||
|
|
@ -1,21 +1,21 @@
|
|||
.About.Dialog {
|
||||
width: $unit * 60;
|
||||
width: $unit * 60;
|
||||
|
||||
section {
|
||||
margin-bottom: $unit;
|
||||
section {
|
||||
margin-bottom: $unit;
|
||||
|
||||
h2 {
|
||||
margin-bottom: $unit * 3;
|
||||
}
|
||||
h2 {
|
||||
margin-bottom: $unit * 3;
|
||||
}
|
||||
}
|
||||
|
||||
.DialogDescription {
|
||||
font-size: $font-regular;
|
||||
line-height: 1.24;
|
||||
margin-bottom: $unit;
|
||||
.DialogDescription {
|
||||
font-size: $font-regular;
|
||||
line-height: 1.24;
|
||||
margin-bottom: $unit;
|
||||
|
||||
&:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
&:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,56 +6,68 @@ import CrossIcon from '~public/icons/Cross.svg'
|
|||
import './index.scss'
|
||||
|
||||
const AboutModal = () => {
|
||||
const { t } = useTranslation('common')
|
||||
const { t } = useTranslation('common')
|
||||
|
||||
return (
|
||||
<Dialog.Root>
|
||||
<Dialog.Trigger asChild>
|
||||
<li className="MenuItem">
|
||||
<span>{t('modals.about.title')}</span>
|
||||
</li>
|
||||
</Dialog.Trigger>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Content className="About Dialog" onOpenAutoFocus={ (event) => event.preventDefault() }>
|
||||
<div className="DialogHeader">
|
||||
<Dialog.Title className="DialogTitle">{t('menu.about')}</Dialog.Title>
|
||||
<Dialog.Close className="DialogClose" asChild>
|
||||
<span>
|
||||
<CrossIcon />
|
||||
</span>
|
||||
</Dialog.Close>
|
||||
</div>
|
||||
return (
|
||||
<Dialog.Root>
|
||||
<Dialog.Trigger asChild>
|
||||
<li className="MenuItem">
|
||||
<span>{t('modals.about.title')}</span>
|
||||
</li>
|
||||
</Dialog.Trigger>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Content
|
||||
className="About Dialog"
|
||||
onOpenAutoFocus={(event) => event.preventDefault()}
|
||||
>
|
||||
<div className="DialogHeader">
|
||||
<Dialog.Title className="DialogTitle">
|
||||
{t('menu.about')}
|
||||
</Dialog.Title>
|
||||
<Dialog.Close className="DialogClose" asChild>
|
||||
<span>
|
||||
<CrossIcon />
|
||||
</span>
|
||||
</Dialog.Close>
|
||||
</div>
|
||||
|
||||
<section>
|
||||
<Dialog.Description className="DialogDescription">
|
||||
Granblue.team is a tool to save and share team compositions for <a href="https://game.granbluefantasy.jp">Granblue Fantasy.</a>
|
||||
</Dialog.Description>
|
||||
<Dialog.Description className="DialogDescription">
|
||||
Start adding things to a team and a URL will be created for you to share it wherever you like, no account needed.
|
||||
</Dialog.Description>
|
||||
<Dialog.Description className="DialogDescription">
|
||||
You can make an account to save any teams you find for future reference, or to keep all of your teams together in one place.
|
||||
</Dialog.Description>
|
||||
</section>
|
||||
<section>
|
||||
<Dialog.Description className="DialogDescription">
|
||||
Granblue.team is a tool to save and share team compositions for{' '}
|
||||
<a href="https://game.granbluefantasy.jp">Granblue Fantasy.</a>
|
||||
</Dialog.Description>
|
||||
<Dialog.Description className="DialogDescription">
|
||||
Start adding things to a team and a URL will be created for you to
|
||||
share it wherever you like, no account needed.
|
||||
</Dialog.Description>
|
||||
<Dialog.Description className="DialogDescription">
|
||||
You can make an account to save any teams you find for future
|
||||
reference, or to keep all of your teams together in one place.
|
||||
</Dialog.Description>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<Dialog.Title className="DialogTitle">Credits</Dialog.Title>
|
||||
<Dialog.Description className="DialogDescription">
|
||||
Granblue.team was built by <a href="https://twitter.com/jedmund">@jedmund</a> with a lot of help from <a href="https://twitter.com/lalalalinna">@lalalalinna</a> and <a href="https://twitter.com/tarngerine">@tarngerine</a>.
|
||||
</Dialog.Description>
|
||||
</section>
|
||||
<section>
|
||||
<Dialog.Title className="DialogTitle">Credits</Dialog.Title>
|
||||
<Dialog.Description className="DialogDescription">
|
||||
Granblue.team was built by{' '}
|
||||
<a href="https://twitter.com/jedmund">@jedmund</a> with a lot of
|
||||
help from{' '}
|
||||
<a href="https://twitter.com/lalalalinna">@lalalalinna</a> and{' '}
|
||||
<a href="https://twitter.com/tarngerine">@tarngerine</a>.
|
||||
</Dialog.Description>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<Dialog.Title className="DialogTitle">Open Source</Dialog.Title>
|
||||
<Dialog.Description className="DialogDescription">
|
||||
This app is open source. You can contribute on Github.
|
||||
</Dialog.Description>
|
||||
</section>
|
||||
</Dialog.Content>
|
||||
<Dialog.Overlay className="Overlay" />
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
)
|
||||
<section>
|
||||
<Dialog.Title className="DialogTitle">Open Source</Dialog.Title>
|
||||
<Dialog.Description className="DialogDescription">
|
||||
This app is open source. You can contribute on Github.
|
||||
</Dialog.Description>
|
||||
</section>
|
||||
</Dialog.Content>
|
||||
<Dialog.Overlay className="Overlay" />
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export default AboutModal
|
||||
export default AboutModal
|
||||
|
|
|
|||
|
|
@ -1,164 +1,142 @@
|
|||
.Account.Dialog {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit * 2;
|
||||
width: $unit * 60;
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit * 2;
|
||||
width: $unit * 60;
|
||||
|
||||
form {
|
||||
.Switch {
|
||||
$height: 34px;
|
||||
background: $grey-70;
|
||||
border-radius: calc($height / 2);
|
||||
border: none;
|
||||
position: relative;
|
||||
width: 58px;
|
||||
height: $height;
|
||||
|
||||
&:focus {
|
||||
box-shadow: 0 0 0 2px $grey-15;
|
||||
}
|
||||
|
||||
&[data-state='checked'] {
|
||||
background: $grey-15;
|
||||
}
|
||||
}
|
||||
|
||||
.Thumb {
|
||||
background: $grey-100;
|
||||
border-radius: 13px;
|
||||
display: block;
|
||||
height: 26px;
|
||||
width: 26px;
|
||||
transition: transform 100ms;
|
||||
transform: translateX(-1px);
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&[data-state='checked'] {
|
||||
background: $grey-100;
|
||||
transform: translateX(21px);
|
||||
}
|
||||
}
|
||||
|
||||
.field {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: $unit * 2;
|
||||
|
||||
select {
|
||||
background: no-repeat url('/icons/ArrowDark.svg'), $grey-90;
|
||||
background-position-y: center;
|
||||
background-position-x: 95%;
|
||||
margin: 0;
|
||||
width: 240px;
|
||||
}
|
||||
|
||||
.left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit * 2;
|
||||
flex-grow: 1;
|
||||
gap: calc($unit / 2);
|
||||
|
||||
.Switch {
|
||||
$height: 34px;
|
||||
background: $grey-70;
|
||||
border-radius: calc($height / 2);
|
||||
border: none;
|
||||
position: relative;
|
||||
width: 58px;
|
||||
height: $height;
|
||||
|
||||
&:focus {
|
||||
box-shadow: 0 0 0 2px $grey-00;
|
||||
}
|
||||
|
||||
&[data-state="checked"] {
|
||||
background: $grey-00;
|
||||
}
|
||||
label {
|
||||
color: var(--text-secondary);
|
||||
font-size: $font-regular;
|
||||
}
|
||||
|
||||
.Thumb {
|
||||
background: white;
|
||||
border-radius: 13px;
|
||||
display: block;
|
||||
height: 26px;
|
||||
width: 26px;
|
||||
transition: transform 100ms;
|
||||
transform: translateX(-1px);
|
||||
p {
|
||||
color: var(--text-secondary);
|
||||
font-size: $font-small;
|
||||
line-height: 1.1;
|
||||
max-width: 300px;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
&.jp {
|
||||
max-width: 270px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&[data-state="checked"] {
|
||||
background: white;
|
||||
transform: translateX(21px);
|
||||
}
|
||||
.preview {
|
||||
$diameter: 48px;
|
||||
background-color: $grey-90;
|
||||
border-radius: 999px;
|
||||
height: $diameter;
|
||||
width: $diameter;
|
||||
|
||||
img {
|
||||
height: $diameter;
|
||||
width: $diameter;
|
||||
}
|
||||
|
||||
.Button {
|
||||
font-size: $font-regular;
|
||||
padding: ($unit * 1.5) ($unit * 2);
|
||||
margin-top: $unit * 2;
|
||||
width: 100%;
|
||||
|
||||
&.btn-disabled {
|
||||
background: $grey-90;
|
||||
color: $grey-70;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&:not(.btn-disabled) {
|
||||
background: $grey-90;
|
||||
color: $grey-40;
|
||||
|
||||
&:hover {
|
||||
background: $grey-80;
|
||||
}
|
||||
}
|
||||
&.fire {
|
||||
background: $fire-bg-20;
|
||||
}
|
||||
|
||||
.field {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: $unit * 2;
|
||||
|
||||
select {
|
||||
background: no-repeat url('/icons/ArrowDark.svg'), $grey-90;
|
||||
background-position-y: center;
|
||||
background-position-x: 95%;
|
||||
margin: 0;
|
||||
width: 240px;
|
||||
}
|
||||
|
||||
.left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
gap: calc($unit / 2);
|
||||
|
||||
label {
|
||||
color: $grey-00;
|
||||
font-size: $font-regular;
|
||||
}
|
||||
|
||||
p {
|
||||
color: $grey-60;
|
||||
font-size: $font-small;
|
||||
line-height: 1.1;
|
||||
max-width: 300px;
|
||||
|
||||
&.jp {
|
||||
max-width: 270px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.preview {
|
||||
$diameter: 48px;
|
||||
background-color: $grey-90;
|
||||
border-radius: 999px;
|
||||
height: $diameter;
|
||||
width: $diameter;
|
||||
|
||||
img {
|
||||
height: $diameter;
|
||||
width: $diameter;
|
||||
}
|
||||
|
||||
&.fire {
|
||||
background: $fire-bg-light;
|
||||
}
|
||||
|
||||
&.water {
|
||||
background: $water-bg-light;
|
||||
}
|
||||
|
||||
&.wind {
|
||||
background: $wind-bg-light;
|
||||
}
|
||||
|
||||
&.earth {
|
||||
background: $earth-bg-light;
|
||||
}
|
||||
|
||||
&.dark {
|
||||
background: $dark-bg-light;
|
||||
}
|
||||
|
||||
&.light {
|
||||
background: $light-bg-light;
|
||||
}
|
||||
}
|
||||
&.water {
|
||||
background: $water-bg-20;
|
||||
}
|
||||
|
||||
section {
|
||||
margin-bottom: $unit;
|
||||
|
||||
h2 {
|
||||
margin-bottom: $unit * 3;
|
||||
}
|
||||
&.wind {
|
||||
background: $wind-bg-20;
|
||||
}
|
||||
|
||||
&.earth {
|
||||
background: $earth-bg-20;
|
||||
}
|
||||
|
||||
&.dark {
|
||||
background: $dark-bg-10;
|
||||
}
|
||||
|
||||
&.light {
|
||||
background: $light-bg-20;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.DialogDescription {
|
||||
font-size: $font-regular;
|
||||
line-height: 1.24;
|
||||
margin-bottom: $unit;
|
||||
section {
|
||||
margin-bottom: $unit;
|
||||
|
||||
&:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
h2 {
|
||||
margin-bottom: $unit * 3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.DialogDescription {
|
||||
font-size: $font-regular;
|
||||
line-height: 1.24;
|
||||
margin-bottom: $unit;
|
||||
|
||||
&:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,31 +1,31 @@
|
|||
import React, { useEffect, useState } from "react"
|
||||
import { getCookie } from "cookies-next"
|
||||
import { useRouter } from "next/router"
|
||||
import { useSnapshot } from "valtio"
|
||||
import { useTranslation } from "next-i18next"
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { getCookie } from 'cookies-next'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
|
||||
import * as Dialog from "@radix-ui/react-dialog"
|
||||
import * as Switch from "@radix-ui/react-switch"
|
||||
import * as Dialog from '@radix-ui/react-dialog'
|
||||
import * as Switch from '@radix-ui/react-switch'
|
||||
|
||||
import api from "~utils/api"
|
||||
import { accountState } from "~utils/accountState"
|
||||
import { pictureData } from "~utils/pictureData"
|
||||
import api from '~utils/api'
|
||||
import { accountState } from '~utils/accountState'
|
||||
import { pictureData } from '~utils/pictureData'
|
||||
|
||||
import Button from "~components/Button"
|
||||
import Button from '~components/Button'
|
||||
|
||||
import CrossIcon from "~public/icons/Cross.svg"
|
||||
import "./index.scss"
|
||||
import CrossIcon from '~public/icons/Cross.svg'
|
||||
import './index.scss'
|
||||
|
||||
const AccountModal = () => {
|
||||
const { account } = useSnapshot(accountState)
|
||||
|
||||
const router = useRouter()
|
||||
const { t } = useTranslation("common")
|
||||
const { t } = useTranslation('common')
|
||||
const locale =
|
||||
router.locale && ["en", "ja"].includes(router.locale) ? router.locale : "en"
|
||||
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
|
||||
|
||||
// Cookies
|
||||
const cookie = getCookie("account")
|
||||
const cookie = getCookie('account')
|
||||
|
||||
const headers = {}
|
||||
// cookies.account != null
|
||||
|
|
@ -38,8 +38,8 @@ const AccountModal = () => {
|
|||
|
||||
// State
|
||||
const [open, setOpen] = useState(false)
|
||||
const [picture, setPicture] = useState("")
|
||||
const [language, setLanguage] = useState("")
|
||||
const [picture, setPicture] = useState('')
|
||||
const [language, setLanguage] = useState('')
|
||||
const [gender, setGender] = useState(0)
|
||||
const [privateProfile, setPrivateProfile] = useState(false)
|
||||
|
||||
|
|
@ -136,7 +136,7 @@ const AccountModal = () => {
|
|||
<Dialog.Root open={open} onOpenChange={openChange}>
|
||||
<Dialog.Trigger asChild>
|
||||
<li className="MenuItem">
|
||||
<span>{t("menu.settings")}</span>
|
||||
<span>{t('menu.settings')}</span>
|
||||
</li>
|
||||
</Dialog.Trigger>
|
||||
<Dialog.Portal>
|
||||
|
|
@ -147,7 +147,7 @@ const AccountModal = () => {
|
|||
<div className="DialogHeader">
|
||||
<div className="DialogTop">
|
||||
<Dialog.Title className="SubTitle">
|
||||
{t("modals.settings.title")}
|
||||
{t('modals.settings.title')}
|
||||
</Dialog.Title>
|
||||
<Dialog.Title className="DialogTitle">
|
||||
@{account.user?.username}
|
||||
|
|
@ -163,7 +163,7 @@ const AccountModal = () => {
|
|||
<form onSubmit={update}>
|
||||
<div className="field">
|
||||
<div className="left">
|
||||
<label>{t("modals.settings.labels.picture")}</label>
|
||||
<label>{t('modals.settings.labels.picture')}</label>
|
||||
</div>
|
||||
|
||||
<div
|
||||
|
|
@ -190,7 +190,7 @@ const AccountModal = () => {
|
|||
</div>
|
||||
<div className="field">
|
||||
<div className="left">
|
||||
<label>{t("modals.settings.labels.gender")}</label>
|
||||
<label>{t('modals.settings.labels.gender')}</label>
|
||||
</div>
|
||||
|
||||
<select
|
||||
|
|
@ -200,16 +200,16 @@ const AccountModal = () => {
|
|||
ref={genderSelect}
|
||||
>
|
||||
<option key="gran" value="0">
|
||||
{t("modals.settings.gender.gran")}
|
||||
{t('modals.settings.gender.gran')}
|
||||
</option>
|
||||
<option key="djeeta" value="1">
|
||||
{t("modals.settings.gender.djeeta")}
|
||||
{t('modals.settings.gender.djeeta')}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="field">
|
||||
<div className="left">
|
||||
<label>{t("modals.settings.labels.language")}</label>
|
||||
<label>{t('modals.settings.labels.language')}</label>
|
||||
</div>
|
||||
|
||||
<select
|
||||
|
|
@ -219,18 +219,18 @@ const AccountModal = () => {
|
|||
ref={languageSelect}
|
||||
>
|
||||
<option key="en" value="en">
|
||||
{t("modals.settings.language.english")}
|
||||
{t('modals.settings.language.english')}
|
||||
</option>
|
||||
<option key="jp" value="ja">
|
||||
{t("modals.settings.language.japanese")}
|
||||
{t('modals.settings.language.japanese')}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="field">
|
||||
<div className="left">
|
||||
<label>{t("modals.settings.labels.private")}</label>
|
||||
<label>{t('modals.settings.labels.private')}</label>
|
||||
<p className={locale}>
|
||||
{t("modals.settings.descriptions.private")}
|
||||
{t('modals.settings.descriptions.private')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -243,7 +243,10 @@ const AccountModal = () => {
|
|||
</Switch.Root>
|
||||
</div>
|
||||
|
||||
<Button>{t("modals.settings.buttons.confirm")}</Button>
|
||||
<Button
|
||||
contained={true}
|
||||
text={t('modals.settings.buttons.confirm')}
|
||||
/>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
<Dialog.Overlay className="Overlay" />
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
}
|
||||
|
||||
.Alert {
|
||||
background: white;
|
||||
background: $grey-100;
|
||||
border-radius: $unit;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
@ -42,7 +42,7 @@
|
|||
|
||||
&:not(.btn-disabled) {
|
||||
background: $grey-90;
|
||||
color: $grey-40;
|
||||
color: $grey-50;
|
||||
|
||||
&:hover {
|
||||
background: $grey-80;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import React from "react"
|
||||
import * as AlertDialog from "@radix-ui/react-alert-dialog"
|
||||
import React from 'react'
|
||||
import * as AlertDialog from '@radix-ui/react-alert-dialog'
|
||||
|
||||
import "./index.scss"
|
||||
import Button from "~components/Button"
|
||||
import { ButtonType } from "~utils/enums"
|
||||
import './index.scss'
|
||||
import Button from '~components/Button'
|
||||
import { ButtonType } from '~utils/enums'
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
|
|
@ -23,7 +23,7 @@ const Alert = (props: Props) => {
|
|||
<AlertDialog.Overlay className="Overlay" onClick={props.cancelAction} />
|
||||
<div className="AlertWrapper">
|
||||
<AlertDialog.Content className="Alert">
|
||||
{props.title ? <AlertDialog.Title>Error</AlertDialog.Title> : ""}
|
||||
{props.title ? <AlertDialog.Title>Error</AlertDialog.Title> : ''}
|
||||
<AlertDialog.Description className="description">
|
||||
{props.message}
|
||||
</AlertDialog.Description>
|
||||
|
|
@ -38,7 +38,7 @@ const Alert = (props: Props) => {
|
|||
{props.primaryActionText}
|
||||
</AlertDialog.Action>
|
||||
) : (
|
||||
""
|
||||
''
|
||||
)}
|
||||
</div>
|
||||
</AlertDialog.Content>
|
||||
|
|
|
|||
|
|
@ -1,48 +1,48 @@
|
|||
.AXSelect {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit;
|
||||
|
||||
.AXSet {
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.errors {
|
||||
color: $error;
|
||||
display: none;
|
||||
padding: $unit 0;
|
||||
|
||||
&.visible {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.fields {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: $unit;
|
||||
|
||||
select {
|
||||
flex-grow: 1;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.Input {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
border: none;
|
||||
background-color: $grey-90;
|
||||
border-radius: 6px;
|
||||
box-sizing: border-box;
|
||||
color: $grey-00;
|
||||
height: $unit * 6;
|
||||
display: block;
|
||||
font-size: $font-regular;
|
||||
padding: $unit;
|
||||
text-align: right;
|
||||
min-width: 100px;
|
||||
width: 100px;
|
||||
}
|
||||
}
|
||||
.AXSet {
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.errors {
|
||||
color: $error;
|
||||
display: none;
|
||||
padding: $unit 0;
|
||||
|
||||
&.visible {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.fields {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: $unit;
|
||||
|
||||
select {
|
||||
flex-grow: 1;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.Input {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
border: none;
|
||||
background-color: $grey-90;
|
||||
border-radius: 6px;
|
||||
box-sizing: border-box;
|
||||
color: $grey-15;
|
||||
height: $unit * 6;
|
||||
display: block;
|
||||
font-size: $font-regular;
|
||||
padding: $unit;
|
||||
text-align: right;
|
||||
min-width: 100px;
|
||||
width: 100px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,258 +9,338 @@ import { axData } from '~utils/axData'
|
|||
import './index.scss'
|
||||
|
||||
interface ErrorMap {
|
||||
[index: string]: string
|
||||
axValue1: string
|
||||
axValue2: string
|
||||
[index: string]: string
|
||||
axValue1: string
|
||||
axValue2: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
axType: number
|
||||
currentSkills?: SimpleAxSkill[],
|
||||
sendValidity: (isValid: boolean) => void
|
||||
sendValues: (primaryAxModifier: number, primaryAxValue: number, secondaryAxModifier: number, secondaryAxValue: number) => void
|
||||
axType: number
|
||||
currentSkills?: SimpleAxSkill[]
|
||||
sendValidity: (isValid: boolean) => void
|
||||
sendValues: (
|
||||
primaryAxModifier: number,
|
||||
primaryAxValue: number,
|
||||
secondaryAxModifier: number,
|
||||
secondaryAxValue: number
|
||||
) => void
|
||||
}
|
||||
|
||||
const AXSelect = (props: Props) => {
|
||||
const router = useRouter()
|
||||
const locale = (router.locale && ['en', 'ja'].includes(router.locale)) ? router.locale : 'en'
|
||||
const { t } = useTranslation('common')
|
||||
const router = useRouter()
|
||||
const locale =
|
||||
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
|
||||
const { t } = useTranslation('common')
|
||||
|
||||
// Set up form states and error handling
|
||||
const [errors, setErrors] = useState<ErrorMap>({
|
||||
axValue1: '',
|
||||
axValue2: ''
|
||||
})
|
||||
// Set up form states and error handling
|
||||
const [errors, setErrors] = useState<ErrorMap>({
|
||||
axValue1: '',
|
||||
axValue2: '',
|
||||
})
|
||||
|
||||
const primaryErrorClasses = classNames({
|
||||
'errors': true,
|
||||
'visible': errors.axValue1.length > 0
|
||||
})
|
||||
const primaryErrorClasses = classNames({
|
||||
errors: true,
|
||||
visible: errors.axValue1.length > 0,
|
||||
})
|
||||
|
||||
const secondaryErrorClasses = classNames({
|
||||
'errors': true,
|
||||
'visible': errors.axValue2.length > 0
|
||||
})
|
||||
const secondaryErrorClasses = classNames({
|
||||
errors: true,
|
||||
visible: errors.axValue2.length > 0,
|
||||
})
|
||||
|
||||
// Refs
|
||||
const primaryAxModifierSelect = React.createRef<HTMLSelectElement>()
|
||||
const primaryAxValueInput = React.createRef<HTMLInputElement>()
|
||||
const secondaryAxModifierSelect = React.createRef<HTMLSelectElement>()
|
||||
const secondaryAxValueInput = React.createRef<HTMLInputElement>()
|
||||
// Refs
|
||||
const primaryAxModifierSelect = React.createRef<HTMLSelectElement>()
|
||||
const primaryAxValueInput = React.createRef<HTMLInputElement>()
|
||||
const secondaryAxModifierSelect = React.createRef<HTMLSelectElement>()
|
||||
const secondaryAxValueInput = React.createRef<HTMLInputElement>()
|
||||
|
||||
// States
|
||||
const [primaryAxModifier, setPrimaryAxModifier] = useState(-1)
|
||||
const [secondaryAxModifier, setSecondaryAxModifier] = useState(-1)
|
||||
const [primaryAxValue, setPrimaryAxValue] = useState(0.0)
|
||||
const [secondaryAxValue, setSecondaryAxValue] = useState(0.0)
|
||||
// States
|
||||
const [primaryAxModifier, setPrimaryAxModifier] = useState(-1)
|
||||
const [secondaryAxModifier, setSecondaryAxModifier] = useState(-1)
|
||||
const [primaryAxValue, setPrimaryAxValue] = useState(0.0)
|
||||
const [secondaryAxValue, setSecondaryAxValue] = useState(0.0)
|
||||
|
||||
useEffect(() => {
|
||||
if (props.currentSkills && props.currentSkills[0]) {
|
||||
if (props.currentSkills[0].modifier != null)
|
||||
setPrimaryAxModifier(props.currentSkills[0].modifier)
|
||||
useEffect(() => {
|
||||
if (props.currentSkills && props.currentSkills[0]) {
|
||||
if (props.currentSkills[0].modifier != null)
|
||||
setPrimaryAxModifier(props.currentSkills[0].modifier)
|
||||
|
||||
setPrimaryAxValue(props.currentSkills[0].strength)
|
||||
}
|
||||
|
||||
if (props.currentSkills && props.currentSkills[1]) {
|
||||
if (props.currentSkills[1].modifier != null)
|
||||
setSecondaryAxModifier(props.currentSkills[1].modifier)
|
||||
|
||||
setSecondaryAxValue(props.currentSkills[1].strength)
|
||||
}
|
||||
}, [props.currentSkills])
|
||||
|
||||
useEffect(() => {
|
||||
props.sendValues(primaryAxModifier, primaryAxValue, secondaryAxModifier, secondaryAxValue)
|
||||
}, [props, primaryAxModifier, primaryAxValue, secondaryAxModifier, secondaryAxValue])
|
||||
|
||||
useEffect(() => {
|
||||
props.sendValidity(primaryAxValue > 0 && errors.axValue1 === '' && errors.axValue2 === '')
|
||||
}, [props, primaryAxValue, errors])
|
||||
|
||||
// Classes
|
||||
const secondarySetClasses = classNames({
|
||||
'AXSet': true,
|
||||
'hidden': primaryAxModifier < 0
|
||||
})
|
||||
|
||||
function generateOptions(modifierSet: number) {
|
||||
const axOptions = axData[props.axType - 1]
|
||||
|
||||
let axOptionElements: React.ReactNode[] = []
|
||||
if (modifierSet == 0) {
|
||||
axOptionElements = axOptions.map((ax, i) => {
|
||||
return (
|
||||
<option key={i} value={ax.id}>{ax.name[locale]}</option>
|
||||
)
|
||||
})
|
||||
} else {
|
||||
// If we are loading data from the server, state doesn't set before render,
|
||||
// so our defaultValue is undefined.
|
||||
let modifier = -1;
|
||||
if (primaryAxModifier >= 0)
|
||||
modifier = primaryAxModifier
|
||||
else if (props.currentSkills)
|
||||
modifier = props.currentSkills[0].modifier
|
||||
|
||||
if (modifier >= 0 && axOptions[modifier]) {
|
||||
const primarySkill = axOptions[modifier]
|
||||
|
||||
if (primarySkill.secondary) {
|
||||
const secondaryAxOptions = primarySkill.secondary
|
||||
axOptionElements = secondaryAxOptions.map((ax, i) => {
|
||||
return (
|
||||
<option key={i} value={ax.id}>{ax.name[locale]}</option>
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
axOptionElements?.unshift(<option key={-1} value={-1}>{t('ax.no_skill')}</option>)
|
||||
return axOptionElements
|
||||
setPrimaryAxValue(props.currentSkills[0].strength)
|
||||
}
|
||||
|
||||
function handleSelectChange(event: React.ChangeEvent<HTMLSelectElement>) {
|
||||
const value = parseInt(event.target.value)
|
||||
if (props.currentSkills && props.currentSkills[1]) {
|
||||
if (props.currentSkills[1].modifier != null)
|
||||
setSecondaryAxModifier(props.currentSkills[1].modifier)
|
||||
|
||||
if (primaryAxModifierSelect.current == event.target) {
|
||||
setPrimaryAxModifier(value)
|
||||
|
||||
if (primaryAxValueInput.current && secondaryAxModifierSelect.current && secondaryAxValueInput.current) {
|
||||
setupInput(axData[props.axType - 1][value], primaryAxValueInput.current)
|
||||
|
||||
secondaryAxModifierSelect.current.value = "-1"
|
||||
secondaryAxValueInput.current.value = ""
|
||||
}
|
||||
} else {
|
||||
setSecondaryAxModifier(value)
|
||||
|
||||
const primaryAxSkill = axData[props.axType - 1][primaryAxModifier]
|
||||
const currentAxSkill = (primaryAxSkill.secondary) ?
|
||||
primaryAxSkill.secondary.find(skill => skill.id == value) : undefined
|
||||
|
||||
if (secondaryAxValueInput.current)
|
||||
setupInput(currentAxSkill, secondaryAxValueInput.current)
|
||||
}
|
||||
setSecondaryAxValue(props.currentSkills[1].strength)
|
||||
}
|
||||
}, [props.currentSkills])
|
||||
|
||||
function handleInputChange(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
const value = parseFloat(event.target.value)
|
||||
let newErrors = {...errors}
|
||||
|
||||
if (primaryAxValueInput.current == event.target) {
|
||||
if (handlePrimaryErrors(value))
|
||||
setPrimaryAxValue(value)
|
||||
} else {
|
||||
if (handleSecondaryErrors(value))
|
||||
setSecondaryAxValue(value)
|
||||
}
|
||||
}
|
||||
|
||||
function handlePrimaryErrors(value: number) {
|
||||
const primaryAxSkill = axData[props.axType - 1][primaryAxModifier]
|
||||
let newErrors = {...errors}
|
||||
|
||||
if (value < primaryAxSkill.minValue) {
|
||||
newErrors.axValue1 = t('ax.errors.value_too_low', {
|
||||
name: primaryAxSkill.name[locale],
|
||||
minValue: primaryAxSkill.minValue,
|
||||
suffix: (primaryAxSkill.suffix) ? primaryAxSkill.suffix : ''
|
||||
})
|
||||
} else if (value > primaryAxSkill.maxValue) {
|
||||
newErrors.axValue1 = t('ax.errors.value_too_high', {
|
||||
name: primaryAxSkill.name[locale],
|
||||
maxValue: primaryAxSkill.minValue,
|
||||
suffix: (primaryAxSkill.suffix) ? primaryAxSkill.suffix : ''
|
||||
})
|
||||
} else if (!value || value <= 0) {
|
||||
newErrors.axValue1 = t('ax.errors.value_empty', { name: primaryAxSkill.name[locale] })
|
||||
} else {
|
||||
newErrors.axValue1 = ''
|
||||
}
|
||||
|
||||
setErrors(newErrors)
|
||||
|
||||
return newErrors.axValue1.length === 0
|
||||
}
|
||||
|
||||
function handleSecondaryErrors(value: number) {
|
||||
const primaryAxSkill = axData[props.axType - 1][primaryAxModifier]
|
||||
let newErrors = {...errors}
|
||||
|
||||
if (primaryAxSkill.secondary) {
|
||||
const secondaryAxSkill = primaryAxSkill.secondary.find(skill => skill.id == secondaryAxModifier)
|
||||
|
||||
if (secondaryAxSkill) {
|
||||
if (value < secondaryAxSkill.minValue) {
|
||||
newErrors.axValue2 = t('ax.errors.value_too_low', {
|
||||
name: secondaryAxSkill.name[locale],
|
||||
minValue: secondaryAxSkill.minValue,
|
||||
suffix: (secondaryAxSkill.suffix) ? secondaryAxSkill.suffix : ''
|
||||
})
|
||||
} else if (value > secondaryAxSkill.maxValue) {
|
||||
newErrors.axValue2 = t('ax.errors.value_too_high', {
|
||||
name: secondaryAxSkill.name[locale],
|
||||
maxValue: secondaryAxSkill.minValue,
|
||||
suffix: (secondaryAxSkill.suffix) ? secondaryAxSkill.suffix : ''
|
||||
})
|
||||
} else if (!secondaryAxSkill.suffix && value % 1 !== 0) {
|
||||
newErrors.axValue2 = t('ax.errors.value_not_whole', { name: secondaryAxSkill.name[locale] })
|
||||
} else if (primaryAxValue <= 0) {
|
||||
newErrors.axValue1 = t('ax.errors.value_empty', { name: primaryAxSkill.name[locale] })
|
||||
} else {
|
||||
newErrors.axValue2 = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setErrors(newErrors)
|
||||
|
||||
return newErrors.axValue2.length === 0
|
||||
}
|
||||
|
||||
function setupInput(ax: AxSkill | undefined, element: HTMLInputElement) {
|
||||
if (ax) {
|
||||
const rangeString = `${ax.minValue}~${ax.maxValue}${ax.suffix || ''}`
|
||||
|
||||
element.disabled = false
|
||||
element.placeholder = rangeString
|
||||
element.min = `${ax.minValue}`
|
||||
element.max = `${ax.maxValue}`
|
||||
element.step = (ax.suffix) ? "0.5" : "1"
|
||||
} else {
|
||||
if (primaryAxValueInput.current && secondaryAxValueInput.current) {
|
||||
if (primaryAxValueInput.current == element) {
|
||||
primaryAxValueInput.current.disabled = true
|
||||
primaryAxValueInput.current.placeholder = ''
|
||||
}
|
||||
|
||||
secondaryAxValueInput.current.disabled = true
|
||||
secondaryAxValueInput.current.placeholder = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="AXSelect">
|
||||
<div className="AXSet">
|
||||
<div className="fields">
|
||||
<select key="ax1" defaultValue={ (props.currentSkills && props.currentSkills[0]) ? props.currentSkills[0].modifier : -1 } onChange={handleSelectChange} ref={primaryAxModifierSelect}>{ generateOptions(0) }</select>
|
||||
<input defaultValue={ (props.currentSkills && props.currentSkills[0]) ? props.currentSkills[0].strength : 0 } className="Input" type="number" onChange={handleInputChange} ref={primaryAxValueInput} disabled={primaryAxValue != 0} />
|
||||
</div>
|
||||
<p className={primaryErrorClasses}>{errors.axValue1}</p>
|
||||
</div>
|
||||
|
||||
<div className={secondarySetClasses}>
|
||||
<div className="fields">
|
||||
<select key="ax2" defaultValue={ (props.currentSkills && props.currentSkills[1]) ? props.currentSkills[1].modifier : -1 } onChange={handleSelectChange} ref={secondaryAxModifierSelect}>{ generateOptions(1) }</select>
|
||||
<input defaultValue={ (props.currentSkills && props.currentSkills[1]) ? props.currentSkills[1].strength : 0 } className="Input" type="number" onChange={handleInputChange} ref={secondaryAxValueInput} disabled={secondaryAxValue != 0} />
|
||||
</div>
|
||||
<p className={secondaryErrorClasses}>{errors.axValue2}</p>
|
||||
</div>
|
||||
</div>
|
||||
useEffect(() => {
|
||||
props.sendValues(
|
||||
primaryAxModifier,
|
||||
primaryAxValue,
|
||||
secondaryAxModifier,
|
||||
secondaryAxValue
|
||||
)
|
||||
}, [
|
||||
props,
|
||||
primaryAxModifier,
|
||||
primaryAxValue,
|
||||
secondaryAxModifier,
|
||||
secondaryAxValue,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
props.sendValidity(
|
||||
primaryAxValue > 0 && errors.axValue1 === '' && errors.axValue2 === ''
|
||||
)
|
||||
}, [props, primaryAxValue, errors])
|
||||
|
||||
// Classes
|
||||
const secondarySetClasses = classNames({
|
||||
AXSet: true,
|
||||
hidden: primaryAxModifier < 0,
|
||||
})
|
||||
|
||||
function generateOptions(modifierSet: number) {
|
||||
const axOptions = axData[props.axType - 1]
|
||||
|
||||
let axOptionElements: React.ReactNode[] = []
|
||||
if (modifierSet == 0) {
|
||||
axOptionElements = axOptions.map((ax, i) => {
|
||||
return (
|
||||
<option key={i} value={ax.id}>
|
||||
{ax.name[locale]}
|
||||
</option>
|
||||
)
|
||||
})
|
||||
} else {
|
||||
// If we are loading data from the server, state doesn't set before render,
|
||||
// so our defaultValue is undefined.
|
||||
let modifier = -1
|
||||
if (primaryAxModifier >= 0) modifier = primaryAxModifier
|
||||
else if (props.currentSkills) modifier = props.currentSkills[0].modifier
|
||||
|
||||
if (modifier >= 0 && axOptions[modifier]) {
|
||||
const primarySkill = axOptions[modifier]
|
||||
|
||||
if (primarySkill.secondary) {
|
||||
const secondaryAxOptions = primarySkill.secondary
|
||||
axOptionElements = secondaryAxOptions.map((ax, i) => {
|
||||
return (
|
||||
<option key={i} value={ax.id}>
|
||||
{ax.name[locale]}
|
||||
</option>
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
axOptionElements?.unshift(
|
||||
<option key={-1} value={-1}>
|
||||
{t('ax.no_skill')}
|
||||
</option>
|
||||
)
|
||||
return axOptionElements
|
||||
}
|
||||
|
||||
function handleSelectChange(event: React.ChangeEvent<HTMLSelectElement>) {
|
||||
const value = parseInt(event.target.value)
|
||||
|
||||
if (primaryAxModifierSelect.current == event.target) {
|
||||
setPrimaryAxModifier(value)
|
||||
|
||||
if (
|
||||
primaryAxValueInput.current &&
|
||||
secondaryAxModifierSelect.current &&
|
||||
secondaryAxValueInput.current
|
||||
) {
|
||||
setupInput(axData[props.axType - 1][value], primaryAxValueInput.current)
|
||||
|
||||
secondaryAxModifierSelect.current.value = '-1'
|
||||
secondaryAxValueInput.current.value = ''
|
||||
}
|
||||
} else {
|
||||
setSecondaryAxModifier(value)
|
||||
|
||||
const primaryAxSkill = axData[props.axType - 1][primaryAxModifier]
|
||||
const currentAxSkill = primaryAxSkill.secondary
|
||||
? primaryAxSkill.secondary.find((skill) => skill.id == value)
|
||||
: undefined
|
||||
|
||||
if (secondaryAxValueInput.current)
|
||||
setupInput(currentAxSkill, secondaryAxValueInput.current)
|
||||
}
|
||||
}
|
||||
|
||||
function handleInputChange(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
const value = parseFloat(event.target.value)
|
||||
let newErrors = { ...errors }
|
||||
|
||||
if (primaryAxValueInput.current == event.target) {
|
||||
if (handlePrimaryErrors(value)) setPrimaryAxValue(value)
|
||||
} else {
|
||||
if (handleSecondaryErrors(value)) setSecondaryAxValue(value)
|
||||
}
|
||||
}
|
||||
|
||||
function handlePrimaryErrors(value: number) {
|
||||
const primaryAxSkill = axData[props.axType - 1][primaryAxModifier]
|
||||
let newErrors = { ...errors }
|
||||
|
||||
if (value < primaryAxSkill.minValue) {
|
||||
newErrors.axValue1 = t('ax.errors.value_too_low', {
|
||||
name: primaryAxSkill.name[locale],
|
||||
minValue: primaryAxSkill.minValue,
|
||||
suffix: primaryAxSkill.suffix ? primaryAxSkill.suffix : '',
|
||||
})
|
||||
} else if (value > primaryAxSkill.maxValue) {
|
||||
newErrors.axValue1 = t('ax.errors.value_too_high', {
|
||||
name: primaryAxSkill.name[locale],
|
||||
maxValue: primaryAxSkill.minValue,
|
||||
suffix: primaryAxSkill.suffix ? primaryAxSkill.suffix : '',
|
||||
})
|
||||
} else if (!value || value <= 0) {
|
||||
newErrors.axValue1 = t('ax.errors.value_empty', {
|
||||
name: primaryAxSkill.name[locale],
|
||||
})
|
||||
} else {
|
||||
newErrors.axValue1 = ''
|
||||
}
|
||||
|
||||
setErrors(newErrors)
|
||||
|
||||
return newErrors.axValue1.length === 0
|
||||
}
|
||||
|
||||
function handleSecondaryErrors(value: number) {
|
||||
const primaryAxSkill = axData[props.axType - 1][primaryAxModifier]
|
||||
let newErrors = { ...errors }
|
||||
|
||||
if (primaryAxSkill.secondary) {
|
||||
const secondaryAxSkill = primaryAxSkill.secondary.find(
|
||||
(skill) => skill.id == secondaryAxModifier
|
||||
)
|
||||
|
||||
if (secondaryAxSkill) {
|
||||
if (value < secondaryAxSkill.minValue) {
|
||||
newErrors.axValue2 = t('ax.errors.value_too_low', {
|
||||
name: secondaryAxSkill.name[locale],
|
||||
minValue: secondaryAxSkill.minValue,
|
||||
suffix: secondaryAxSkill.suffix ? secondaryAxSkill.suffix : '',
|
||||
})
|
||||
} else if (value > secondaryAxSkill.maxValue) {
|
||||
newErrors.axValue2 = t('ax.errors.value_too_high', {
|
||||
name: secondaryAxSkill.name[locale],
|
||||
maxValue: secondaryAxSkill.minValue,
|
||||
suffix: secondaryAxSkill.suffix ? secondaryAxSkill.suffix : '',
|
||||
})
|
||||
} else if (!secondaryAxSkill.suffix && value % 1 !== 0) {
|
||||
newErrors.axValue2 = t('ax.errors.value_not_whole', {
|
||||
name: secondaryAxSkill.name[locale],
|
||||
})
|
||||
} else if (primaryAxValue <= 0) {
|
||||
newErrors.axValue1 = t('ax.errors.value_empty', {
|
||||
name: primaryAxSkill.name[locale],
|
||||
})
|
||||
} else {
|
||||
newErrors.axValue2 = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setErrors(newErrors)
|
||||
|
||||
return newErrors.axValue2.length === 0
|
||||
}
|
||||
|
||||
function setupInput(ax: AxSkill | undefined, element: HTMLInputElement) {
|
||||
if (ax) {
|
||||
const rangeString = `${ax.minValue}~${ax.maxValue}${ax.suffix || ''}`
|
||||
|
||||
element.disabled = false
|
||||
element.placeholder = rangeString
|
||||
element.min = `${ax.minValue}`
|
||||
element.max = `${ax.maxValue}`
|
||||
element.step = ax.suffix ? '0.5' : '1'
|
||||
} else {
|
||||
if (primaryAxValueInput.current && secondaryAxValueInput.current) {
|
||||
if (primaryAxValueInput.current == element) {
|
||||
primaryAxValueInput.current.disabled = true
|
||||
primaryAxValueInput.current.placeholder = ''
|
||||
}
|
||||
|
||||
secondaryAxValueInput.current.disabled = true
|
||||
secondaryAxValueInput.current.placeholder = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="AXSelect">
|
||||
<div className="AXSet">
|
||||
<div className="fields">
|
||||
<select
|
||||
key="ax1"
|
||||
defaultValue={
|
||||
props.currentSkills && props.currentSkills[0]
|
||||
? props.currentSkills[0].modifier
|
||||
: -1
|
||||
}
|
||||
onChange={handleSelectChange}
|
||||
ref={primaryAxModifierSelect}
|
||||
>
|
||||
{generateOptions(0)}
|
||||
</select>
|
||||
<input
|
||||
defaultValue={
|
||||
props.currentSkills && props.currentSkills[0]
|
||||
? props.currentSkills[0].strength
|
||||
: 0
|
||||
}
|
||||
className="Input"
|
||||
type="number"
|
||||
onChange={handleInputChange}
|
||||
ref={primaryAxValueInput}
|
||||
disabled={primaryAxValue != 0}
|
||||
/>
|
||||
</div>
|
||||
<p className={primaryErrorClasses}>{errors.axValue1}</p>
|
||||
</div>
|
||||
|
||||
<div className={secondarySetClasses}>
|
||||
<div className="fields">
|
||||
<select
|
||||
key="ax2"
|
||||
defaultValue={
|
||||
props.currentSkills && props.currentSkills[1]
|
||||
? props.currentSkills[1].modifier
|
||||
: -1
|
||||
}
|
||||
onChange={handleSelectChange}
|
||||
ref={secondaryAxModifierSelect}
|
||||
>
|
||||
{generateOptions(1)}
|
||||
</select>
|
||||
<input
|
||||
defaultValue={
|
||||
props.currentSkills && props.currentSkills[1]
|
||||
? props.currentSkills[1].strength
|
||||
: 0
|
||||
}
|
||||
className="Input"
|
||||
type="number"
|
||||
onChange={handleInputChange}
|
||||
ref={secondaryAxValueInput}
|
||||
disabled={secondaryAxValue != 0}
|
||||
/>
|
||||
</div>
|
||||
<p className={secondaryErrorClasses}>{errors.axValue2}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AXSelect
|
||||
export default AXSelect
|
||||
|
|
|
|||
|
|
@ -1,214 +1,260 @@
|
|||
.Button {
|
||||
align-items: center;
|
||||
align-items: center;
|
||||
background: var(--button-bg);
|
||||
border: none;
|
||||
border-radius: $input-corner;
|
||||
color: var(--button-text);
|
||||
display: inline-flex;
|
||||
font-size: $font-button;
|
||||
font-weight: $normal;
|
||||
gap: 6px;
|
||||
|
||||
&:hover,
|
||||
&.Blended:hover {
|
||||
background: var(--button-bg-hover);
|
||||
cursor: pointer;
|
||||
color: var(--button-text-hover);
|
||||
|
||||
.Accessory svg {
|
||||
fill: var(--button-text-hover);
|
||||
}
|
||||
|
||||
.Accessory svg.stroke {
|
||||
fill: none;
|
||||
stroke: var(--button-text-hover);
|
||||
}
|
||||
}
|
||||
|
||||
&.Blended {
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
color: $grey-50;
|
||||
display: inline-flex;
|
||||
font-size: $font-button;
|
||||
font-weight: $normal;
|
||||
gap: 6px;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
&.Contained {
|
||||
background: var(--button-contained-bg);
|
||||
|
||||
&:hover {
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
color: $grey-00;
|
||||
|
||||
.icon svg {
|
||||
fill: $grey-00;
|
||||
}
|
||||
|
||||
.icon.stroke svg {
|
||||
fill: none;
|
||||
stroke: $grey-00;
|
||||
}
|
||||
background: var(--button-contained-bg-hover);
|
||||
}
|
||||
|
||||
&.destructive:hover {
|
||||
background: $error;
|
||||
color: white;
|
||||
|
||||
.icon svg {
|
||||
fill: white;
|
||||
}
|
||||
&.Save:hover .Accessory svg {
|
||||
fill: #ff4d4d;
|
||||
stroke: #ff4d4d;
|
||||
}
|
||||
|
||||
&.save:hover {
|
||||
color: #FF4D4D;
|
||||
|
||||
.icon svg {
|
||||
fill: #FF4D4D;
|
||||
stroke: #FF4D4D;
|
||||
&.Active.Save {
|
||||
color: #ff4d4d;
|
||||
|
||||
.Accessory svg {
|
||||
fill: #ff4d4d;
|
||||
stroke: #ff4d4d;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: darken(#ff4d4d, 30);
|
||||
|
||||
.Accessory svg {
|
||||
fill: darken(#ff4d4d, 30);
|
||||
stroke: darken(#ff4d4d, 30);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.medium {
|
||||
height: $unit * 5.5;
|
||||
padding: ($unit * 1.5) $unit-2x;
|
||||
}
|
||||
|
||||
&.small {
|
||||
padding: $unit * 1.5;
|
||||
}
|
||||
|
||||
&.destructive:hover {
|
||||
background: $error;
|
||||
color: $grey-100;
|
||||
|
||||
.Accessory svg {
|
||||
fill: $grey-100;
|
||||
}
|
||||
}
|
||||
|
||||
&.save:hover {
|
||||
color: #ff4d4d;
|
||||
|
||||
.Accessory svg {
|
||||
fill: #ff4d4d;
|
||||
stroke: #ff4d4d;
|
||||
}
|
||||
}
|
||||
|
||||
&.save.Active {
|
||||
color: #ff4d4d;
|
||||
|
||||
&:hover {
|
||||
color: darken(#ff4d4d, 30);
|
||||
|
||||
.icon svg {
|
||||
fill: darken(#ff4d4d, 30);
|
||||
stroke: darken(#ff4d4d, 30);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.modal:hover {
|
||||
background: $grey-90;
|
||||
}
|
||||
|
||||
&.modal.destructive {
|
||||
color: $error;
|
||||
|
||||
&:hover {
|
||||
color: darken($error, 10);
|
||||
}
|
||||
}
|
||||
|
||||
.Accessory {
|
||||
$dimension: $unit-2x;
|
||||
|
||||
display: flex;
|
||||
|
||||
svg {
|
||||
fill: var(--button-text);
|
||||
height: $dimension;
|
||||
width: $dimension;
|
||||
|
||||
&.stroke {
|
||||
fill: none;
|
||||
stroke: var(--button-text);
|
||||
}
|
||||
|
||||
&.Add {
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
}
|
||||
|
||||
&.Check {
|
||||
height: 22px;
|
||||
width: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
&.save.Active {
|
||||
color: #FF4D4D;
|
||||
|
||||
.icon svg {
|
||||
fill: #FF4D4D;
|
||||
stroke: #FF4D4D;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: darken(#FF4D4D, 30);
|
||||
|
||||
.icon svg {
|
||||
fill: darken(#FF4D4D, 30);
|
||||
stroke: darken(#FF4D4D, 30);
|
||||
}
|
||||
}
|
||||
&.check svg {
|
||||
margin-top: 1px;
|
||||
height: 14px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
&.modal:hover {
|
||||
background: $grey-90;
|
||||
svg &.settings svg {
|
||||
height: 13px;
|
||||
width: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
&.modal.destructive {
|
||||
color: $error;
|
||||
|
||||
&:hover {
|
||||
color: darken($error, 10)
|
||||
}
|
||||
&.btn-blue {
|
||||
background: $blue;
|
||||
color: #8b8b8b;
|
||||
|
||||
&:hover {
|
||||
background: #4b9be5;
|
||||
color: #233e56;
|
||||
}
|
||||
}
|
||||
|
||||
&.btn-red {
|
||||
background: #fa4242;
|
||||
color: #860f0f;
|
||||
|
||||
&:hover {
|
||||
background: #e91a1a;
|
||||
color: #4e1717;
|
||||
|
||||
.icon {
|
||||
color: #4e1717;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-top: 2px;
|
||||
|
||||
svg {
|
||||
fill: $grey-50;
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
}
|
||||
|
||||
&.check svg {
|
||||
margin-top: 1px;
|
||||
height: 14px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
&.stroke svg {
|
||||
fill: none;
|
||||
stroke: $grey-50;
|
||||
}
|
||||
|
||||
&.settings svg {
|
||||
height: 13px;
|
||||
width: 13px;
|
||||
}
|
||||
color: #860f0f;
|
||||
}
|
||||
}
|
||||
|
||||
&.Active {
|
||||
background: white;
|
||||
&.btn-disabled {
|
||||
background: #e0e0e0;
|
||||
color: #bababa;
|
||||
|
||||
&:hover {
|
||||
background: #e0e0e0;
|
||||
color: #bababa;
|
||||
}
|
||||
}
|
||||
|
||||
&.btn-blue {
|
||||
background: $blue;
|
||||
color: #8b8b8b;
|
||||
&.null {
|
||||
background: $grey-90;
|
||||
color: $grey-55;
|
||||
|
||||
&:hover {
|
||||
background: #4B9BE5;
|
||||
color: #233E56;
|
||||
}
|
||||
&:hover {
|
||||
background: $grey-70;
|
||||
color: $grey-15;
|
||||
}
|
||||
}
|
||||
|
||||
&.btn-red {
|
||||
background: #fa4242;
|
||||
color: #860f0f;
|
||||
&.wind {
|
||||
background: $wind-bg-20;
|
||||
color: $wind-text-10;
|
||||
|
||||
&:hover {
|
||||
background: #e91a1a;
|
||||
color: #4e1717;
|
||||
|
||||
.icon {
|
||||
color: #4e1717;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
color: #860f0f;
|
||||
}
|
||||
&:hover {
|
||||
background: darken($wind-bg-20, 10);
|
||||
}
|
||||
}
|
||||
|
||||
&.btn-disabled {
|
||||
background: #e0e0e0;
|
||||
color: #bababa;
|
||||
&.fire {
|
||||
background: $fire-bg-20;
|
||||
color: $fire-text-10;
|
||||
|
||||
&:hover {
|
||||
background: #e0e0e0;
|
||||
color: #bababa;
|
||||
}
|
||||
&:hover {
|
||||
background: darken($fire-bg-20, 10);
|
||||
}
|
||||
}
|
||||
|
||||
&.null {
|
||||
background: $grey-90;
|
||||
color: $grey-50;
|
||||
&.water {
|
||||
background: $water-bg-20;
|
||||
color: $water-text-10;
|
||||
|
||||
&:hover {
|
||||
background: $grey-70;
|
||||
color: $grey-00;
|
||||
}
|
||||
&:hover {
|
||||
background: darken($water-bg-20, 10);
|
||||
}
|
||||
}
|
||||
|
||||
&.wind {
|
||||
background: $wind-bg-light;
|
||||
color: $wind-text-dark;
|
||||
&.earth {
|
||||
background: $earth-bg-20;
|
||||
color: $earth-text-10;
|
||||
|
||||
&:hover {
|
||||
background: darken($wind-bg-light, 10);
|
||||
}
|
||||
&:hover {
|
||||
background: darken($earth-bg-20, 10);
|
||||
}
|
||||
}
|
||||
|
||||
&.fire {
|
||||
background: $fire-bg-light;
|
||||
color: $fire-text-dark;
|
||||
&.dark {
|
||||
background: $dark-bg-10;
|
||||
color: $dark-text-10;
|
||||
|
||||
&:hover {
|
||||
background: darken($fire-bg-light, 10);
|
||||
}
|
||||
&:hover {
|
||||
background: darken($dark-bg-10, 10);
|
||||
}
|
||||
}
|
||||
|
||||
&.water {
|
||||
background: $water-bg-light;
|
||||
color: $water-text-dark;
|
||||
&.light {
|
||||
background: $light-bg-20;
|
||||
color: $light-text-10;
|
||||
|
||||
&:hover {
|
||||
background: darken($water-bg-light, 10);
|
||||
}
|
||||
&:hover {
|
||||
background: darken($light-bg-20, 10);
|
||||
}
|
||||
}
|
||||
|
||||
&.earth {
|
||||
background: $earth-bg-light;
|
||||
color: $earth-text-dark;
|
||||
|
||||
&:hover {
|
||||
background: darken($earth-bg-light, 10);
|
||||
}
|
||||
}
|
||||
|
||||
&.dark {
|
||||
background: $dark-bg-light;
|
||||
color: $dark-text-dark;
|
||||
|
||||
&:hover {
|
||||
background: darken($dark-bg-light, 10);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&.light {
|
||||
background: $light-bg-light;
|
||||
color: $light-text-dark;
|
||||
|
||||
&:hover {
|
||||
background: darken($light-bg-light, 10);
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
color: inherit;
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
.Text {
|
||||
color: inherit;
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useEffect, useState } from 'react'
|
||||
import React, { PropsWithChildren, useEffect, useState } from 'react'
|
||||
import classNames from 'classnames'
|
||||
|
||||
import Link from 'next/link'
|
||||
|
|
@ -15,127 +15,174 @@ import SettingsIcon from '~public/icons/Settings.svg'
|
|||
import './index.scss'
|
||||
|
||||
import { ButtonType } from '~utils/enums'
|
||||
import { access } from 'fs'
|
||||
|
||||
interface Props {
|
||||
active?: boolean
|
||||
disabled?: boolean
|
||||
classes?: string[],
|
||||
icon?: string
|
||||
type?: ButtonType
|
||||
children?: React.ReactNode
|
||||
onClick?: (event: React.MouseEvent<HTMLElement>) => void
|
||||
interface Props
|
||||
extends React.DetailedHTMLProps<
|
||||
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
HTMLButtonElement
|
||||
> {
|
||||
accessoryIcon?: React.ReactNode
|
||||
active?: boolean
|
||||
blended?: boolean
|
||||
contained?: boolean
|
||||
size?: 'small' | 'medium' | 'large'
|
||||
text?: string
|
||||
}
|
||||
|
||||
const Button = (props: Props) => {
|
||||
// States
|
||||
const [active, setActive] = useState(false)
|
||||
const [disabled, setDisabled] = useState(false)
|
||||
const [pressed, setPressed] = useState(false)
|
||||
const [buttonType, setButtonType] = useState(ButtonType.Base)
|
||||
|
||||
const classes = classNames({
|
||||
Button: true,
|
||||
'Active': active,
|
||||
'btn-pressed': pressed,
|
||||
'btn-disabled': disabled,
|
||||
'save': props.icon === 'save',
|
||||
'destructive': props.type == ButtonType.Destructive
|
||||
}, props.classes)
|
||||
|
||||
useEffect(() => {
|
||||
if (props.active) setActive(props.active)
|
||||
if (props.disabled) setDisabled(props.disabled)
|
||||
if (props.type) setButtonType(props.type)
|
||||
}, [props.active, props.disabled, props.type])
|
||||
|
||||
const addIcon = (
|
||||
<span className='icon'>
|
||||
<AddIcon />
|
||||
</span>
|
||||
)
|
||||
|
||||
const menuIcon = (
|
||||
<span className='icon'>
|
||||
<MenuIcon />
|
||||
</span>
|
||||
)
|
||||
|
||||
const linkIcon = (
|
||||
<span className='icon stroke'>
|
||||
<LinkIcon />
|
||||
</span>
|
||||
)
|
||||
|
||||
const checkIcon = (
|
||||
<span className='icon check'>
|
||||
<CheckIcon />
|
||||
</span>
|
||||
)
|
||||
|
||||
const crossIcon = (
|
||||
<span className='icon'>
|
||||
<CrossIcon />
|
||||
</span>
|
||||
)
|
||||
|
||||
const editIcon = (
|
||||
<span className='icon'>
|
||||
<EditIcon />
|
||||
</span>
|
||||
)
|
||||
|
||||
const saveIcon = (
|
||||
<span className='icon stroke'>
|
||||
<SaveIcon />
|
||||
</span>
|
||||
)
|
||||
|
||||
const settingsIcon = (
|
||||
<span className='icon settings'>
|
||||
<SettingsIcon />
|
||||
</span>
|
||||
)
|
||||
|
||||
function getIcon() {
|
||||
let icon: React.ReactNode
|
||||
|
||||
switch(props.icon) {
|
||||
case 'new': icon = addIcon; break
|
||||
case 'menu': icon = menuIcon; break
|
||||
case 'link': icon = linkIcon; break
|
||||
case 'check': icon = checkIcon; break
|
||||
case 'cross': icon = crossIcon; break
|
||||
case 'edit': icon = editIcon; break
|
||||
case 'save': icon = saveIcon; break
|
||||
case 'settings': icon = settingsIcon; break
|
||||
}
|
||||
|
||||
return icon
|
||||
}
|
||||
|
||||
function handleMouseDown() {
|
||||
setPressed(true)
|
||||
}
|
||||
|
||||
function handleMouseUp() {
|
||||
setPressed(false)
|
||||
}
|
||||
return (
|
||||
<button
|
||||
className={classes}
|
||||
disabled={disabled}
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseUp={handleMouseUp}
|
||||
onClick={props.onClick}>
|
||||
{ getIcon() }
|
||||
|
||||
{ (props.type != ButtonType.IconOnly) ?
|
||||
<span className='text'>
|
||||
{ props.children }
|
||||
</span> : ''
|
||||
}
|
||||
</button>
|
||||
)
|
||||
const defaultProps = {
|
||||
active: false,
|
||||
blended: false,
|
||||
contained: false,
|
||||
size: 'medium',
|
||||
}
|
||||
|
||||
export default Button
|
||||
const Button = React.forwardRef<HTMLButtonElement, Props>(function button(
|
||||
{ accessoryIcon, active, blended, contained, size, text, ...props },
|
||||
forwardedRef
|
||||
) {
|
||||
const classes = classNames(
|
||||
{
|
||||
Button: true,
|
||||
Active: active,
|
||||
Blended: blended,
|
||||
Contained: contained,
|
||||
// 'btn-pressed': pressed,
|
||||
// 'btn-disabled': disabled,
|
||||
// save: props.icon === 'save',
|
||||
// destructive: props.type == ButtonType.Destructive,
|
||||
},
|
||||
size,
|
||||
props.className
|
||||
)
|
||||
|
||||
const hasAccessory = () => {
|
||||
if (accessoryIcon) return <span className="Accessory">{accessoryIcon}</span>
|
||||
}
|
||||
|
||||
const hasText = () => {
|
||||
if (text) return <span className="Text">{text}</span>
|
||||
}
|
||||
|
||||
return (
|
||||
<button {...props} className={classes} ref={forwardedRef}>
|
||||
{hasAccessory()}
|
||||
{hasText()}
|
||||
</button>
|
||||
)
|
||||
|
||||
// useEffect(() => {
|
||||
// if (props.type) setButtonType(props.type)
|
||||
// }, [props.type])
|
||||
|
||||
// const addIcon = (
|
||||
// <span className="icon">
|
||||
// <AddIcon />
|
||||
// </span>
|
||||
// )
|
||||
|
||||
// const menuIcon = (
|
||||
// <span className="icon">
|
||||
// <MenuIcon />
|
||||
// </span>
|
||||
// )
|
||||
|
||||
// const linkIcon = (
|
||||
// <span className="icon stroke">
|
||||
// <LinkIcon />
|
||||
// </span>
|
||||
// )
|
||||
|
||||
// const checkIcon = (
|
||||
// <span className="icon check">
|
||||
// <CheckIcon />
|
||||
// </span>
|
||||
// )
|
||||
|
||||
// const crossIcon = (
|
||||
// <span className="icon">
|
||||
// <CrossIcon />
|
||||
// </span>
|
||||
// )
|
||||
|
||||
// const editIcon = (
|
||||
// <span className="icon">
|
||||
// <EditIcon />
|
||||
// </span>
|
||||
// )
|
||||
|
||||
// const saveIcon = (
|
||||
// <span className="icon stroke">
|
||||
// <SaveIcon />
|
||||
// </span>
|
||||
// )
|
||||
|
||||
// const settingsIcon = (
|
||||
// <span className="icon settings">
|
||||
// <SettingsIcon />
|
||||
// </span>
|
||||
// )
|
||||
|
||||
// function getIcon() {
|
||||
// let icon: React.ReactNode
|
||||
|
||||
// switch (props.icon) {
|
||||
// case 'new':
|
||||
// icon = addIcon
|
||||
// break
|
||||
// case 'menu':
|
||||
// icon = menuIcon
|
||||
// break
|
||||
// case 'link':
|
||||
// icon = linkIcon
|
||||
// break
|
||||
// case 'check':
|
||||
// icon = checkIcon
|
||||
// break
|
||||
// case 'cross':
|
||||
// icon = crossIcon
|
||||
// break
|
||||
// case 'edit':
|
||||
// icon = editIcon
|
||||
// break
|
||||
// case 'save':
|
||||
// icon = saveIcon
|
||||
// break
|
||||
// case 'settings':
|
||||
// icon = settingsIcon
|
||||
// break
|
||||
// }
|
||||
|
||||
// return icon
|
||||
// }
|
||||
|
||||
// function handleMouseDown() {
|
||||
// setPressed(true)
|
||||
// }
|
||||
|
||||
// function handleMouseUp() {
|
||||
// setPressed(false)
|
||||
// }
|
||||
// return (
|
||||
// <button
|
||||
// className={classes}
|
||||
// disabled={disabled}
|
||||
// onMouseDown={handleMouseDown}
|
||||
// onMouseUp={handleMouseUp}
|
||||
// ref={forwardedRef}
|
||||
// {...props}
|
||||
// >
|
||||
// {getIcon()}
|
||||
|
||||
// {props.type != ButtonType.IconOnly ? (
|
||||
// <span className="text">{children}</span>
|
||||
// ) : (
|
||||
// ''
|
||||
// )}
|
||||
// </button>
|
||||
// )
|
||||
})
|
||||
|
||||
Button.defaultProps = defaultProps
|
||||
|
||||
export default Button
|
||||
|
|
|
|||
|
|
@ -1,29 +1,34 @@
|
|||
.Limited {
|
||||
background: white;
|
||||
border-radius: 6px;
|
||||
border: 2px solid transparent;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
gap: $unit;
|
||||
padding-right: $unit * 2;
|
||||
$offset: 2px;
|
||||
|
||||
&:focus-within {
|
||||
border: 2px solid $blue;
|
||||
box-shadow: 0 2px rgba(255, 255, 255, 1);
|
||||
background: var(--input-bg);
|
||||
border-radius: $input-corner;
|
||||
border: $offset solid transparent;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
gap: $unit;
|
||||
padding-top: 2px;
|
||||
padding-bottom: 2px;
|
||||
padding-right: calc($unit-2x - $offset);
|
||||
|
||||
&:focus-within {
|
||||
border: $offset solid $blue;
|
||||
// box-shadow: 0 2px rgba(255, 255, 255, 1);
|
||||
}
|
||||
|
||||
.Counter {
|
||||
color: $grey-55;
|
||||
font-weight: $bold;
|
||||
line-height: 42px;
|
||||
}
|
||||
|
||||
.Input {
|
||||
background: transparent;
|
||||
border-radius: 0;
|
||||
padding-left: calc($unit-2x - $offset);
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.Counter {
|
||||
color: $grey-50;
|
||||
font-weight: $bold;
|
||||
line-height: 42px;
|
||||
}
|
||||
|
||||
.Input {
|
||||
background: transparent;
|
||||
border-radius: 0;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,53 +2,56 @@ import React, { useEffect, useState } from 'react'
|
|||
import './index.scss'
|
||||
|
||||
interface Props {
|
||||
fieldName: string
|
||||
placeholder: string
|
||||
value?: string
|
||||
limit: number
|
||||
error: string
|
||||
onBlur?: (event: React.ChangeEvent<HTMLInputElement>) => void
|
||||
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void
|
||||
fieldName: string
|
||||
placeholder: string
|
||||
value?: string
|
||||
limit: number
|
||||
error: string
|
||||
onBlur?: (event: React.ChangeEvent<HTMLInputElement>) => void
|
||||
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void
|
||||
}
|
||||
|
||||
const CharLimitedFieldset = React.forwardRef<HTMLInputElement, Props>(function useFieldSet(props, ref) {
|
||||
const fieldType = (['password', 'confirm_password'].includes(props.fieldName)) ? 'password' : 'text'
|
||||
const CharLimitedFieldset = React.forwardRef<HTMLInputElement, Props>(
|
||||
function useFieldSet(props, ref) {
|
||||
const fieldType = ['password', 'confirm_password'].includes(props.fieldName)
|
||||
? 'password'
|
||||
: 'text'
|
||||
|
||||
const [currentCount, setCurrentCount] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentCount((props.value) ? props.limit - props.value.length : props.limit)
|
||||
setCurrentCount(
|
||||
props.value ? props.limit - props.value.length : props.limit
|
||||
)
|
||||
}, [props.limit, props.value])
|
||||
|
||||
function onChange(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
setCurrentCount(props.limit - event.currentTarget.value.length)
|
||||
if (props.onChange) props.onChange(event)
|
||||
setCurrentCount(props.limit - event.currentTarget.value.length)
|
||||
if (props.onChange) props.onChange(event)
|
||||
}
|
||||
|
||||
return (
|
||||
<fieldset className="Fieldset">
|
||||
<div className="Limited">
|
||||
<input
|
||||
autoComplete="off"
|
||||
className="Input"
|
||||
type={fieldType}
|
||||
name={props.fieldName}
|
||||
placeholder={props.placeholder}
|
||||
defaultValue={props.value || ''}
|
||||
onBlur={props.onBlur}
|
||||
onChange={onChange}
|
||||
maxLength={props.limit}
|
||||
ref={ref}
|
||||
formNoValidate
|
||||
/>
|
||||
<span className="Counter">{currentCount}</span>
|
||||
</div>
|
||||
{
|
||||
props.error.length > 0 &&
|
||||
<p className='InputError'>{props.error}</p>
|
||||
}
|
||||
</fieldset>
|
||||
<fieldset className="Fieldset">
|
||||
<div className="Limited">
|
||||
<input
|
||||
autoComplete="off"
|
||||
className="Input"
|
||||
type={fieldType}
|
||||
name={props.fieldName}
|
||||
placeholder={props.placeholder}
|
||||
defaultValue={props.value || ''}
|
||||
onBlur={props.onBlur}
|
||||
onChange={onChange}
|
||||
maxLength={props.limit}
|
||||
ref={ref}
|
||||
formNoValidate
|
||||
/>
|
||||
<span className="Counter">{currentCount}</span>
|
||||
</div>
|
||||
{props.error.length > 0 && <p className="InputError">{props.error}</p>}
|
||||
</fieldset>
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
export default CharLimitedFieldset
|
||||
export default CharLimitedFieldset
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
}
|
||||
|
||||
.arrow {
|
||||
color: $grey-50;
|
||||
color: $grey-55;
|
||||
font-size: 4rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
|
@ -52,7 +52,7 @@
|
|||
|
||||
&:not(.btn-disabled) {
|
||||
background: $grey-90;
|
||||
color: $grey-40;
|
||||
color: $grey-50;
|
||||
|
||||
&:hover {
|
||||
background: $grey-80;
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
import React, { useEffect, useState } from "react"
|
||||
import { setCookie } from "cookies-next"
|
||||
import Router, { useRouter } from "next/router"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { AxiosResponse } from "axios"
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { setCookie } from 'cookies-next'
|
||||
import Router, { useRouter } from 'next/router'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { AxiosResponse } from 'axios'
|
||||
|
||||
import * as Dialog from "@radix-ui/react-dialog"
|
||||
import * as Dialog from '@radix-ui/react-dialog'
|
||||
|
||||
import api from "~utils/api"
|
||||
import { appState } from "~utils/appState"
|
||||
import { accountState } from "~utils/accountState"
|
||||
import api from '~utils/api'
|
||||
import { appState } from '~utils/appState'
|
||||
import { accountState } from '~utils/accountState'
|
||||
|
||||
import Button from "~components/Button"
|
||||
import Button from '~components/Button'
|
||||
|
||||
import "./index.scss"
|
||||
import './index.scss'
|
||||
|
||||
interface Props {
|
||||
open: boolean
|
||||
|
|
@ -24,7 +24,7 @@ interface Props {
|
|||
}
|
||||
|
||||
const CharacterConflictModal = (props: Props) => {
|
||||
const { t } = useTranslation("common")
|
||||
const { t } = useTranslation('common')
|
||||
|
||||
// States
|
||||
const [open, setOpen] = useState(false)
|
||||
|
|
@ -35,13 +35,13 @@ const CharacterConflictModal = (props: Props) => {
|
|||
|
||||
function imageUrl(character?: Character, uncap: number = 0) {
|
||||
// Change the image based on the uncap level
|
||||
let suffix = "01"
|
||||
if (uncap == 6) suffix = "04"
|
||||
else if (uncap == 5) suffix = "03"
|
||||
else if (uncap > 2) suffix = "02"
|
||||
let suffix = '01'
|
||||
if (uncap == 6) suffix = '04'
|
||||
else if (uncap == 5) suffix = '03'
|
||||
else if (uncap > 2) suffix = '02'
|
||||
|
||||
// Special casing for Lyria (and Young Cat eventually)
|
||||
if (character?.granblue_id === "3030182000") {
|
||||
if (character?.granblue_id === '3030182000') {
|
||||
let element = 1
|
||||
if (
|
||||
appState.grid.weapons.mainWeapon &&
|
||||
|
|
|
|||
|
|
@ -1,31 +1,31 @@
|
|||
#CharacterGrid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
margin: auto;
|
||||
max-width: 761px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
margin: auto;
|
||||
max-width: 761px;
|
||||
}
|
||||
|
||||
#grid_characters {
|
||||
display: flex;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
max-width: 761px;
|
||||
display: flex;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
max-width: 761px;
|
||||
|
||||
@media (max-width: $medium-screen) {
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
& > * {
|
||||
margin-right: $unit * 3;
|
||||
|
||||
@media (max-width: $medium-screen) {
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
margin-right: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
& > * {
|
||||
margin-right: $unit * 3;
|
||||
|
||||
@media (max-width: $medium-screen) {
|
||||
margin-right: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
& > li:last-child {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
& > li:last-child {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,22 @@
|
|||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react"
|
||||
import { getCookie } from "cookies-next"
|
||||
import { useSnapshot } from "valtio"
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { getCookie } from 'cookies-next'
|
||||
import { useSnapshot } from 'valtio'
|
||||
|
||||
import { AxiosResponse } from "axios"
|
||||
import debounce from "lodash.debounce"
|
||||
import { AxiosResponse } from 'axios'
|
||||
import debounce from 'lodash.debounce'
|
||||
|
||||
import Alert from "~components/Alert"
|
||||
import JobSection from "~components/JobSection"
|
||||
import CharacterUnit from "~components/CharacterUnit"
|
||||
import CharacterConflictModal from "~components/CharacterConflictModal"
|
||||
import Alert from '~components/Alert'
|
||||
import JobSection from '~components/JobSection'
|
||||
import CharacterUnit from '~components/CharacterUnit'
|
||||
import CharacterConflictModal from '~components/CharacterConflictModal'
|
||||
|
||||
import type { JobSkillObject, SearchableObject } from "~types"
|
||||
import type { JobSkillObject, SearchableObject } from '~types'
|
||||
|
||||
import api from "~utils/api"
|
||||
import { appState } from "~utils/appState"
|
||||
import api from '~utils/api'
|
||||
import { appState } from '~utils/appState'
|
||||
|
||||
import "./index.scss"
|
||||
import './index.scss'
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
|
|
@ -31,7 +31,7 @@ const CharacterGrid = (props: Props) => {
|
|||
const numCharacters: number = 5
|
||||
|
||||
// Cookies
|
||||
const cookie = getCookie("account")
|
||||
const cookie = getCookie('account')
|
||||
const accountData: AccountCookie = cookie
|
||||
? JSON.parse(cookie as string)
|
||||
: null
|
||||
|
|
@ -57,7 +57,7 @@ const CharacterGrid = (props: Props) => {
|
|||
2: undefined,
|
||||
3: undefined,
|
||||
})
|
||||
const [errorMessage, setErrorMessage] = useState("")
|
||||
const [errorMessage, setErrorMessage] = useState('')
|
||||
|
||||
// Create a temporary state to store previous character uncap values
|
||||
const [previousUncapValues, setPreviousUncapValues] = useState<{
|
||||
|
|
@ -116,7 +116,7 @@ const CharacterGrid = (props: Props) => {
|
|||
}
|
||||
|
||||
async function handleCharacterResponse(data: any) {
|
||||
if (data.hasOwnProperty("conflicts")) {
|
||||
if (data.hasOwnProperty('conflicts')) {
|
||||
setIncoming(data.incoming)
|
||||
setConflicts(data.conflicts)
|
||||
setPosition(data.position)
|
||||
|
|
@ -185,7 +185,7 @@ const CharacterGrid = (props: Props) => {
|
|||
const saveJob = function (job: Job) {
|
||||
const payload = {
|
||||
party: {
|
||||
job_id: job ? job.id : "",
|
||||
job_id: job ? job.id : '',
|
||||
},
|
||||
...headers,
|
||||
}
|
||||
|
|
@ -231,9 +231,9 @@ const CharacterGrid = (props: Props) => {
|
|||
})
|
||||
.catch((error) => {
|
||||
const data = error.response.data
|
||||
if (data.code == "too_many_skills_of_type") {
|
||||
if (data.code == 'too_many_skills_of_type') {
|
||||
const message = `You can only add up to 2 ${
|
||||
data.skill_type === "emp"
|
||||
data.skill_type === 'emp'
|
||||
? data.skill_type.toUpperCase()
|
||||
: data.skill_type
|
||||
} skills to your party at once.`
|
||||
|
|
@ -268,7 +268,7 @@ const CharacterGrid = (props: Props) => {
|
|||
|
||||
try {
|
||||
if (uncapLevel != previousUncapValues[position])
|
||||
await api.updateUncap("character", id, uncapLevel).then((response) => {
|
||||
await api.updateUncap('character', id, uncapLevel).then((response) => {
|
||||
storeGridCharacter(response.data.grid_character)
|
||||
})
|
||||
} catch (error) {
|
||||
|
|
@ -332,7 +332,7 @@ const CharacterGrid = (props: Props) => {
|
|||
}
|
||||
|
||||
function cancelAlert() {
|
||||
setErrorMessage("")
|
||||
setErrorMessage('')
|
||||
}
|
||||
|
||||
// Render: JSX components
|
||||
|
|
@ -342,7 +342,7 @@ const CharacterGrid = (props: Props) => {
|
|||
open={errorMessage.length > 0}
|
||||
message={errorMessage}
|
||||
cancelAction={cancelAlert}
|
||||
cancelActionText={"Got it"}
|
||||
cancelActionText={'Got it'}
|
||||
/>
|
||||
<div id="CharacterGrid">
|
||||
<JobSection
|
||||
|
|
|
|||
|
|
@ -10,83 +10,114 @@ import UncapIndicator from '~components/UncapIndicator'
|
|||
import './index.scss'
|
||||
|
||||
interface Props {
|
||||
gridCharacter: GridCharacter
|
||||
children: React.ReactNode
|
||||
gridCharacter: GridCharacter
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
interface KeyNames {
|
||||
[key: string]: {
|
||||
en: string,
|
||||
jp: string
|
||||
}
|
||||
[key: string]: {
|
||||
en: string
|
||||
jp: string
|
||||
}
|
||||
}
|
||||
|
||||
const CharacterHovercard = (props: Props) => {
|
||||
const router = useRouter()
|
||||
const { t } = useTranslation('common')
|
||||
const locale = (router.locale && ['en', 'ja'].includes(router.locale)) ? router.locale : 'en'
|
||||
const router = useRouter()
|
||||
const { t } = useTranslation('common')
|
||||
const locale =
|
||||
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
|
||||
|
||||
const Element = ['null', 'wind', 'fire', 'water', 'earth', 'dark', 'light']
|
||||
const Proficiency = ['none', 'sword', 'dagger', 'axe', 'spear', 'bow', 'staff', 'fist', 'harp', 'gun', 'katana']
|
||||
const Element = ['null', 'wind', 'fire', 'water', 'earth', 'dark', 'light']
|
||||
const Proficiency = [
|
||||
'none',
|
||||
'sword',
|
||||
'dagger',
|
||||
'axe',
|
||||
'spear',
|
||||
'bow',
|
||||
'staff',
|
||||
'fist',
|
||||
'harp',
|
||||
'gun',
|
||||
'katana',
|
||||
]
|
||||
|
||||
const tintElement = Element[props.gridCharacter.object.element]
|
||||
const wikiUrl = `https://gbf.wiki/${props.gridCharacter.object.name.en.replaceAll(' ', '_')}`
|
||||
const tintElement = Element[props.gridCharacter.object.element]
|
||||
const wikiUrl = `https://gbf.wiki/${props.gridCharacter.object.name.en.replaceAll(
|
||||
' ',
|
||||
'_'
|
||||
)}`
|
||||
|
||||
function characterImage() {
|
||||
let imgSrc = ""
|
||||
|
||||
if (props.gridCharacter) {
|
||||
const character = props.gridCharacter.object
|
||||
function characterImage() {
|
||||
let imgSrc = ''
|
||||
|
||||
// Change the image based on the uncap level
|
||||
let suffix = '01'
|
||||
if (props.gridCharacter.uncap_level == 6)
|
||||
suffix = '04'
|
||||
else if (props.gridCharacter.uncap_level == 5)
|
||||
suffix = '03'
|
||||
else if (props.gridCharacter.uncap_level > 2)
|
||||
suffix = '02'
|
||||
if (props.gridCharacter) {
|
||||
const character = props.gridCharacter.object
|
||||
|
||||
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/chara-grid/${character.granblue_id}_${suffix}.jpg`
|
||||
}
|
||||
// Change the image based on the uncap level
|
||||
let suffix = '01'
|
||||
if (props.gridCharacter.uncap_level == 6) suffix = '04'
|
||||
else if (props.gridCharacter.uncap_level == 5) suffix = '03'
|
||||
else if (props.gridCharacter.uncap_level > 2) suffix = '02'
|
||||
|
||||
return imgSrc
|
||||
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/chara-grid/${character.granblue_id}_${suffix}.jpg`
|
||||
}
|
||||
|
||||
return (
|
||||
<HoverCard.Root>
|
||||
<HoverCard.Trigger>
|
||||
{ props.children }
|
||||
</HoverCard.Trigger>
|
||||
<HoverCard.Content className="Weapon Hovercard">
|
||||
<div className="top">
|
||||
<div className="title">
|
||||
<h4>{ props.gridCharacter.object.name[locale] }</h4>
|
||||
<img alt={props.gridCharacter.object.name[locale]} src={characterImage()} />
|
||||
</div>
|
||||
<div className="subInfo">
|
||||
<div className="icons">
|
||||
<WeaponLabelIcon labelType={Element[props.gridCharacter.object.element]} />
|
||||
<WeaponLabelIcon labelType={ Proficiency[props.gridCharacter.object.proficiency.proficiency1] } />
|
||||
{ (props.gridCharacter.object.proficiency.proficiency2) ?
|
||||
<WeaponLabelIcon labelType={ Proficiency[props.gridCharacter.object.proficiency.proficiency2] } />
|
||||
: ''}
|
||||
</div>
|
||||
<UncapIndicator
|
||||
type="character"
|
||||
ulb={props.gridCharacter.object.uncap.ulb || false}
|
||||
flb={props.gridCharacter.object.uncap.flb || false}
|
||||
special={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
return imgSrc
|
||||
}
|
||||
|
||||
<a className={`Button ${tintElement}`} href={wikiUrl} target="_new">{t('buttons.wiki')}</a>
|
||||
<HoverCard.Arrow />
|
||||
</HoverCard.Content>
|
||||
</HoverCard.Root>
|
||||
)
|
||||
return (
|
||||
<HoverCard.Root>
|
||||
<HoverCard.Trigger>{props.children}</HoverCard.Trigger>
|
||||
<HoverCard.Content className="Weapon Hovercard">
|
||||
<div className="top">
|
||||
<div className="title">
|
||||
<h4>{props.gridCharacter.object.name[locale]}</h4>
|
||||
<img
|
||||
alt={props.gridCharacter.object.name[locale]}
|
||||
src={characterImage()}
|
||||
/>
|
||||
</div>
|
||||
<div className="subInfo">
|
||||
<div className="icons">
|
||||
<WeaponLabelIcon
|
||||
labelType={Element[props.gridCharacter.object.element]}
|
||||
/>
|
||||
<WeaponLabelIcon
|
||||
labelType={
|
||||
Proficiency[
|
||||
props.gridCharacter.object.proficiency.proficiency1
|
||||
]
|
||||
}
|
||||
/>
|
||||
{props.gridCharacter.object.proficiency.proficiency2 ? (
|
||||
<WeaponLabelIcon
|
||||
labelType={
|
||||
Proficiency[
|
||||
props.gridCharacter.object.proficiency.proficiency2
|
||||
]
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</div>
|
||||
<UncapIndicator
|
||||
type="character"
|
||||
ulb={props.gridCharacter.object.uncap.ulb || false}
|
||||
flb={props.gridCharacter.object.uncap.flb || false}
|
||||
special={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a className={`Button ${tintElement}`} href={wikiUrl} target="_new">
|
||||
{t('buttons.wiki')}
|
||||
</a>
|
||||
<HoverCard.Arrow />
|
||||
</HoverCard.Content>
|
||||
</HoverCard.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export default CharacterHovercard
|
||||
|
||||
|
|
|
|||
|
|
@ -1,63 +1,67 @@
|
|||
.CharacterResult {
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
gap: $unit;
|
||||
padding: $unit * 1.5;
|
||||
|
||||
&:hover {
|
||||
background: var(--button-contained-bg);
|
||||
cursor: pointer;
|
||||
|
||||
.Info h5 {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
background: var(--card-bg);
|
||||
border-radius: 6px;
|
||||
display: inline-block;
|
||||
height: 72px;
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
.Info {
|
||||
display: flex;
|
||||
gap: $unit;
|
||||
padding: $unit * 1.5;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
gap: $unit-half;
|
||||
|
||||
&:hover {
|
||||
background: $grey-90;
|
||||
cursor: pointer;
|
||||
h5 {
|
||||
color: var(--text-secondary);
|
||||
display: inline-block;
|
||||
font-size: $font-medium;
|
||||
font-weight: $medium;
|
||||
}
|
||||
|
||||
img {
|
||||
background: $grey-80;
|
||||
border-radius: 6px;
|
||||
display: inline-block;
|
||||
height: 72px;
|
||||
width: 120px;
|
||||
.UncapIndicator {
|
||||
justify-content: left;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.Info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
gap: calc($unit / 2);
|
||||
.stars {
|
||||
display: inline-block;
|
||||
color: #ffa15e;
|
||||
font-size: $font-xlarge;
|
||||
|
||||
h5 {
|
||||
color: #555;
|
||||
display: inline-block;
|
||||
font-size: $font-medium;
|
||||
font-weight: $medium;
|
||||
}
|
||||
|
||||
.UncapIndicator {
|
||||
justify-content: left;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.stars {
|
||||
display: inline-block;
|
||||
color: #FFA15E;
|
||||
font-size: $font-xlarge;
|
||||
|
||||
& > span {
|
||||
color: #65DAFF;
|
||||
}
|
||||
}
|
||||
|
||||
.tags {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: calc($unit / 2);
|
||||
|
||||
.WeaponLabelIcon {
|
||||
$aspect-ratio: calc(25 / 60);
|
||||
$height: 22px;
|
||||
background-size: calc($height / $aspect-ratio) $height;
|
||||
background-repeat: no-repeat;
|
||||
height: $height;
|
||||
width: calc($height/ $aspect-ratio);
|
||||
}
|
||||
}
|
||||
& > span {
|
||||
color: #65daff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tags {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: calc($unit / 2);
|
||||
|
||||
.WeaponLabelIcon {
|
||||
$aspect-ratio: calc(25 / 60);
|
||||
$height: 22px;
|
||||
background-size: calc($height / $aspect-ratio) $height;
|
||||
background-repeat: no-repeat;
|
||||
height: $height;
|
||||
width: calc($height/ $aspect-ratio);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,45 +7,46 @@ import WeaponLabelIcon from '~components/WeaponLabelIcon'
|
|||
import './index.scss'
|
||||
|
||||
interface Props {
|
||||
data: Character
|
||||
onClick: () => void
|
||||
data: Character
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
const Element = ['null', 'wind', 'fire', 'water', 'earth', 'dark', 'light']
|
||||
|
||||
const CharacterResult = (props: Props) => {
|
||||
const router = useRouter()
|
||||
const locale = (router.locale && ['en', 'ja'].includes(router.locale)) ? router.locale : 'en'
|
||||
const router = useRouter()
|
||||
const locale =
|
||||
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
|
||||
|
||||
const character = props.data
|
||||
const character = props.data
|
||||
|
||||
const characterUrl = () => {
|
||||
let url = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/chara-grid/${character.granblue_id}_01.jpg`
|
||||
const characterUrl = () => {
|
||||
let url = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/chara-grid/${character.granblue_id}_01.jpg`
|
||||
|
||||
if (character.granblue_id === '3030182000') {
|
||||
url = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/chara-grid/${character.granblue_id}_01_01.jpg`
|
||||
}
|
||||
|
||||
return url
|
||||
if (character.granblue_id === '3030182000') {
|
||||
url = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/chara-grid/${character.granblue_id}_01_01.jpg`
|
||||
}
|
||||
|
||||
return (
|
||||
<li className="CharacterResult" onClick={props.onClick}>
|
||||
<img alt={character.name[locale]} src={characterUrl()} />
|
||||
<div className="Info">
|
||||
<h5>{character.name[locale]}</h5>
|
||||
<UncapIndicator
|
||||
type="character"
|
||||
flb={character.uncap.flb}
|
||||
ulb={character.uncap.ulb}
|
||||
special={character.special}
|
||||
/>
|
||||
<div className="tags">
|
||||
<WeaponLabelIcon labelType={Element[character.element]} />
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
return url
|
||||
}
|
||||
|
||||
return (
|
||||
<li className="CharacterResult" onClick={props.onClick}>
|
||||
<img alt={character.name[locale]} src={characterUrl()} />
|
||||
<div className="Info">
|
||||
<h5>{character.name[locale]}</h5>
|
||||
<UncapIndicator
|
||||
type="character"
|
||||
flb={character.uncap.flb}
|
||||
ulb={character.uncap.ulb}
|
||||
special={character.special}
|
||||
/>
|
||||
<div className="tags">
|
||||
<WeaponLabelIcon labelType={Element[character.element]} />
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
export default CharacterResult
|
||||
export default CharacterResult
|
||||
|
|
|
|||
|
|
@ -9,197 +9,260 @@ import SearchFilter from '~components/SearchFilter'
|
|||
import SearchFilterCheckboxItem from '~components/SearchFilterCheckboxItem'
|
||||
|
||||
import './index.scss'
|
||||
import { emptyElementState, emptyProficiencyState, emptyRarityState } from '~utils/emptyStates'
|
||||
import {
|
||||
emptyElementState,
|
||||
emptyProficiencyState,
|
||||
emptyRarityState,
|
||||
} from '~utils/emptyStates'
|
||||
import { elements, proficiencies, rarities } from '~utils/stateValues'
|
||||
|
||||
interface Props {
|
||||
sendFilters: (filters: { [key: string]: number[] }) => void
|
||||
sendFilters: (filters: { [key: string]: number[] }) => void
|
||||
}
|
||||
|
||||
const CharacterSearchFilterBar = (props: Props) => {
|
||||
const { t } = useTranslation('common')
|
||||
const { t } = useTranslation('common')
|
||||
|
||||
const [rarityMenu, setRarityMenu] = useState(false)
|
||||
const [elementMenu, setElementMenu] = useState(false)
|
||||
const [proficiency1Menu, setProficiency1Menu] = useState(false)
|
||||
const [proficiency2Menu, setProficiency2Menu] = useState(false)
|
||||
const [rarityMenu, setRarityMenu] = useState(false)
|
||||
const [elementMenu, setElementMenu] = useState(false)
|
||||
const [proficiency1Menu, setProficiency1Menu] = useState(false)
|
||||
const [proficiency2Menu, setProficiency2Menu] = useState(false)
|
||||
|
||||
const [rarityState, setRarityState] = useState<RarityState>(emptyRarityState)
|
||||
const [elementState, setElementState] = useState<ElementState>(emptyElementState)
|
||||
const [proficiency1State, setProficiency1State] = useState<ProficiencyState>(emptyProficiencyState)
|
||||
const [proficiency2State, setProficiency2State] = useState<ProficiencyState>(emptyProficiencyState)
|
||||
const [rarityState, setRarityState] = useState<RarityState>(emptyRarityState)
|
||||
const [elementState, setElementState] =
|
||||
useState<ElementState>(emptyElementState)
|
||||
const [proficiency1State, setProficiency1State] = useState<ProficiencyState>(
|
||||
emptyProficiencyState
|
||||
)
|
||||
const [proficiency2State, setProficiency2State] = useState<ProficiencyState>(
|
||||
emptyProficiencyState
|
||||
)
|
||||
|
||||
function rarityMenuOpened(open: boolean) {
|
||||
if (open) {
|
||||
setRarityMenu(true)
|
||||
setElementMenu(false)
|
||||
setProficiency1Menu(false)
|
||||
setProficiency2Menu(false)
|
||||
} else setRarityMenu(false)
|
||||
function rarityMenuOpened(open: boolean) {
|
||||
if (open) {
|
||||
setRarityMenu(true)
|
||||
setElementMenu(false)
|
||||
setProficiency1Menu(false)
|
||||
setProficiency2Menu(false)
|
||||
} else setRarityMenu(false)
|
||||
}
|
||||
|
||||
function elementMenuOpened(open: boolean) {
|
||||
if (open) {
|
||||
setRarityMenu(false)
|
||||
setElementMenu(true)
|
||||
setProficiency1Menu(false)
|
||||
setProficiency2Menu(false)
|
||||
} else setElementMenu(false)
|
||||
}
|
||||
|
||||
function proficiency1MenuOpened(open: boolean) {
|
||||
if (open) {
|
||||
setRarityMenu(false)
|
||||
setElementMenu(false)
|
||||
setProficiency1Menu(true)
|
||||
setProficiency2Menu(false)
|
||||
} else setProficiency1Menu(false)
|
||||
}
|
||||
|
||||
function proficiency2MenuOpened(open: boolean) {
|
||||
if (open) {
|
||||
setRarityMenu(false)
|
||||
setElementMenu(false)
|
||||
setProficiency1Menu(false)
|
||||
setProficiency2Menu(true)
|
||||
} else setProficiency2Menu(false)
|
||||
}
|
||||
|
||||
function handleRarityChange(checked: boolean, key: string) {
|
||||
let newRarityState = cloneDeep(rarityState)
|
||||
newRarityState[key].checked = checked
|
||||
setRarityState(newRarityState)
|
||||
}
|
||||
|
||||
function handleElementChange(checked: boolean, key: string) {
|
||||
let newElementState = cloneDeep(elementState)
|
||||
newElementState[key].checked = checked
|
||||
setElementState(newElementState)
|
||||
}
|
||||
|
||||
function handleProficiency1Change(checked: boolean, key: string) {
|
||||
let newProficiencyState = cloneDeep(proficiency1State)
|
||||
newProficiencyState[key].checked = checked
|
||||
setProficiency1State(newProficiencyState)
|
||||
}
|
||||
|
||||
function handleProficiency2Change(checked: boolean, key: string) {
|
||||
let newProficiencyState = cloneDeep(proficiency2State)
|
||||
newProficiencyState[key].checked = checked
|
||||
setProficiency2State(newProficiencyState)
|
||||
}
|
||||
|
||||
function sendFilters() {
|
||||
const checkedRarityFilters = Object.values(rarityState)
|
||||
.filter((x) => x.checked)
|
||||
.map((x, i) => x.id)
|
||||
const checkedElementFilters = Object.values(elementState)
|
||||
.filter((x) => x.checked)
|
||||
.map((x, i) => x.id)
|
||||
const checkedProficiency1Filters = Object.values(proficiency1State)
|
||||
.filter((x) => x.checked)
|
||||
.map((x, i) => x.id)
|
||||
const checkedProficiency2Filters = Object.values(proficiency2State)
|
||||
.filter((x) => x.checked)
|
||||
.map((x, i) => x.id)
|
||||
|
||||
const filters = {
|
||||
rarity: checkedRarityFilters,
|
||||
element: checkedElementFilters,
|
||||
proficiency1: checkedProficiency1Filters,
|
||||
proficiency2: checkedProficiency2Filters,
|
||||
}
|
||||
|
||||
function elementMenuOpened(open: boolean) {
|
||||
if (open) {
|
||||
setRarityMenu(false)
|
||||
setElementMenu(true)
|
||||
setProficiency1Menu(false)
|
||||
setProficiency2Menu(false)
|
||||
} else setElementMenu(false)
|
||||
}
|
||||
props.sendFilters(filters)
|
||||
}
|
||||
|
||||
function proficiency1MenuOpened(open: boolean) {
|
||||
if (open) {
|
||||
setRarityMenu(false)
|
||||
setElementMenu(false)
|
||||
setProficiency1Menu(true)
|
||||
setProficiency2Menu(false)
|
||||
} else setProficiency1Menu(false)
|
||||
}
|
||||
useEffect(() => {
|
||||
sendFilters()
|
||||
}, [rarityState, elementState, proficiency1State, proficiency2State])
|
||||
|
||||
function proficiency2MenuOpened(open: boolean) {
|
||||
if (open) {
|
||||
setRarityMenu(false)
|
||||
setElementMenu(false)
|
||||
setProficiency1Menu(false)
|
||||
setProficiency2Menu(true)
|
||||
} else setProficiency2Menu(false)
|
||||
}
|
||||
|
||||
function handleRarityChange(checked: boolean, key: string) {
|
||||
let newRarityState = cloneDeep(rarityState)
|
||||
newRarityState[key].checked = checked
|
||||
setRarityState(newRarityState)
|
||||
}
|
||||
|
||||
function handleElementChange(checked: boolean, key: string) {
|
||||
let newElementState = cloneDeep(elementState)
|
||||
newElementState[key].checked = checked
|
||||
setElementState(newElementState)
|
||||
}
|
||||
|
||||
function handleProficiency1Change(checked: boolean, key: string) {
|
||||
let newProficiencyState = cloneDeep(proficiency1State)
|
||||
newProficiencyState[key].checked = checked
|
||||
setProficiency1State(newProficiencyState)
|
||||
}
|
||||
|
||||
function handleProficiency2Change(checked: boolean, key: string) {
|
||||
let newProficiencyState = cloneDeep(proficiency2State)
|
||||
newProficiencyState[key].checked = checked
|
||||
setProficiency2State(newProficiencyState)
|
||||
}
|
||||
|
||||
function sendFilters() {
|
||||
const checkedRarityFilters = Object.values(rarityState).filter(x => x.checked).map((x, i) => x.id)
|
||||
const checkedElementFilters = Object.values(elementState).filter(x => x.checked).map((x, i) => x.id)
|
||||
const checkedProficiency1Filters = Object.values(proficiency1State).filter(x => x.checked).map((x, i) => x.id)
|
||||
const checkedProficiency2Filters = Object.values(proficiency2State).filter(x => x.checked).map((x, i) => x.id)
|
||||
|
||||
const filters = {
|
||||
rarity: checkedRarityFilters,
|
||||
element: checkedElementFilters,
|
||||
proficiency1: checkedProficiency1Filters,
|
||||
proficiency2: checkedProficiency2Filters
|
||||
}
|
||||
|
||||
props.sendFilters(filters)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
sendFilters()
|
||||
}, [rarityState, elementState, proficiency1State, proficiency2State])
|
||||
|
||||
function renderProficiencyFilter(proficiency: 1 | 2) {
|
||||
const onCheckedChange = (proficiency == 1) ? handleProficiency1Change : handleProficiency2Change
|
||||
const numSelected = (proficiency == 1)
|
||||
? Object.values(proficiency1State).map(x => x.checked).filter(Boolean).length
|
||||
: Object.values(proficiency2State).map(x => x.checked).filter(Boolean).length
|
||||
const open = (proficiency == 1) ? proficiency1Menu : proficiency2Menu
|
||||
const onOpenChange = (proficiency == 1) ? proficiency1MenuOpened : proficiency2MenuOpened
|
||||
|
||||
return (
|
||||
<SearchFilter
|
||||
label={`${t('filters.labels.proficiency')} ${proficiency}`}
|
||||
numSelected={numSelected}
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}>
|
||||
<DropdownMenu.Label className="Label">{`${t('filters.labels.proficiency')} ${proficiency}`}</DropdownMenu.Label>
|
||||
<section>
|
||||
<DropdownMenu.Group className="Group">
|
||||
{ Array.from(Array(proficiencies.length / 2)).map((x, i) => {
|
||||
const checked = (proficiency == 1)
|
||||
? proficiency1State[proficiencies[i]].checked
|
||||
: proficiency2State[proficiencies[i]].checked
|
||||
|
||||
return (
|
||||
<SearchFilterCheckboxItem
|
||||
key={proficiencies[i]}
|
||||
onCheckedChange={onCheckedChange}
|
||||
checked={checked}
|
||||
valueKey={proficiencies[i]}>
|
||||
{t(`proficiencies.${proficiencies[i]}`)}
|
||||
</SearchFilterCheckboxItem>
|
||||
)}
|
||||
) }
|
||||
</DropdownMenu.Group>
|
||||
<DropdownMenu.Group className="Group">
|
||||
{ Array.from(Array(proficiencies.length / 2)).map((x, i) => {
|
||||
const checked = (proficiency == 1)
|
||||
? proficiency1State[proficiencies[i + (proficiencies.length / 2)]].checked
|
||||
: proficiency2State[proficiencies[i + (proficiencies.length / 2)]].checked
|
||||
|
||||
return (
|
||||
<SearchFilterCheckboxItem
|
||||
key={proficiencies[i + (proficiencies.length / 2)]}
|
||||
onCheckedChange={onCheckedChange}
|
||||
checked={checked}
|
||||
valueKey={proficiencies[i + (proficiencies.length / 2)]}>
|
||||
{t(`proficiencies.${proficiencies[i + (proficiencies.length / 2)]}`)}
|
||||
</SearchFilterCheckboxItem>
|
||||
)}
|
||||
) }
|
||||
</DropdownMenu.Group>
|
||||
</section>
|
||||
</SearchFilter>
|
||||
)
|
||||
}
|
||||
function renderProficiencyFilter(proficiency: 1 | 2) {
|
||||
const onCheckedChange =
|
||||
proficiency == 1 ? handleProficiency1Change : handleProficiency2Change
|
||||
const numSelected =
|
||||
proficiency == 1
|
||||
? Object.values(proficiency1State)
|
||||
.map((x) => x.checked)
|
||||
.filter(Boolean).length
|
||||
: Object.values(proficiency2State)
|
||||
.map((x) => x.checked)
|
||||
.filter(Boolean).length
|
||||
const open = proficiency == 1 ? proficiency1Menu : proficiency2Menu
|
||||
const onOpenChange =
|
||||
proficiency == 1 ? proficiency1MenuOpened : proficiency2MenuOpened
|
||||
|
||||
return (
|
||||
<div className="SearchFilterBar">
|
||||
<SearchFilter label={t('filters.labels.rarity')} numSelected={Object.values(rarityState).map(x => x.checked).filter(Boolean).length} open={rarityMenu} onOpenChange={rarityMenuOpened}>
|
||||
<DropdownMenu.Label className="Label">{t('filters.labels.rarity')}</DropdownMenu.Label>
|
||||
{ Array.from(Array(rarities.length)).map((x, i) => {
|
||||
return (
|
||||
<SearchFilterCheckboxItem
|
||||
key={rarities[i]}
|
||||
onCheckedChange={handleRarityChange}
|
||||
checked={rarityState[rarities[i]].checked}
|
||||
valueKey={rarities[i]}>
|
||||
{t(`rarities.${rarities[i]}`)}
|
||||
</SearchFilterCheckboxItem>
|
||||
)}
|
||||
) }
|
||||
</SearchFilter>
|
||||
<SearchFilter
|
||||
label={`${t('filters.labels.proficiency')} ${proficiency}`}
|
||||
numSelected={numSelected}
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
>
|
||||
<DropdownMenu.Label className="Label">{`${t(
|
||||
'filters.labels.proficiency'
|
||||
)} ${proficiency}`}</DropdownMenu.Label>
|
||||
<section>
|
||||
<DropdownMenu.Group className="Group">
|
||||
{Array.from(Array(proficiencies.length / 2)).map((x, i) => {
|
||||
const checked =
|
||||
proficiency == 1
|
||||
? proficiency1State[proficiencies[i]].checked
|
||||
: proficiency2State[proficiencies[i]].checked
|
||||
|
||||
<SearchFilter label={t('filters.labels.element')} numSelected={Object.values(elementState).map(x => x.checked).filter(Boolean).length} open={elementMenu} onOpenChange={elementMenuOpened}>
|
||||
<DropdownMenu.Label className="Label">{t('filters.labels.element')}</DropdownMenu.Label>
|
||||
{ Array.from(Array(elements.length)).map((x, i) => {
|
||||
return (
|
||||
<SearchFilterCheckboxItem
|
||||
key={elements[i]}
|
||||
onCheckedChange={handleElementChange}
|
||||
checked={elementState[elements[i]].checked}
|
||||
valueKey={elements[i]}>
|
||||
{t(`elements.${elements[i]}`)}
|
||||
</SearchFilterCheckboxItem>
|
||||
)}
|
||||
) }
|
||||
</SearchFilter>
|
||||
return (
|
||||
<SearchFilterCheckboxItem
|
||||
key={proficiencies[i]}
|
||||
onCheckedChange={onCheckedChange}
|
||||
checked={checked}
|
||||
valueKey={proficiencies[i]}
|
||||
>
|
||||
{t(`proficiencies.${proficiencies[i]}`)}
|
||||
</SearchFilterCheckboxItem>
|
||||
)
|
||||
})}
|
||||
</DropdownMenu.Group>
|
||||
<DropdownMenu.Group className="Group">
|
||||
{Array.from(Array(proficiencies.length / 2)).map((x, i) => {
|
||||
const checked =
|
||||
proficiency == 1
|
||||
? proficiency1State[
|
||||
proficiencies[i + proficiencies.length / 2]
|
||||
].checked
|
||||
: proficiency2State[
|
||||
proficiencies[i + proficiencies.length / 2]
|
||||
].checked
|
||||
|
||||
{ renderProficiencyFilter(1) }
|
||||
{ renderProficiencyFilter(2) }
|
||||
</div>
|
||||
return (
|
||||
<SearchFilterCheckboxItem
|
||||
key={proficiencies[i + proficiencies.length / 2]}
|
||||
onCheckedChange={onCheckedChange}
|
||||
checked={checked}
|
||||
valueKey={proficiencies[i + proficiencies.length / 2]}
|
||||
>
|
||||
{t(
|
||||
`proficiencies.${
|
||||
proficiencies[i + proficiencies.length / 2]
|
||||
}`
|
||||
)}
|
||||
</SearchFilterCheckboxItem>
|
||||
)
|
||||
})}
|
||||
</DropdownMenu.Group>
|
||||
</section>
|
||||
</SearchFilter>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="SearchFilterBar">
|
||||
<SearchFilter
|
||||
label={t('filters.labels.rarity')}
|
||||
numSelected={
|
||||
Object.values(rarityState)
|
||||
.map((x) => x.checked)
|
||||
.filter(Boolean).length
|
||||
}
|
||||
open={rarityMenu}
|
||||
onOpenChange={rarityMenuOpened}
|
||||
>
|
||||
<DropdownMenu.Label className="Label">
|
||||
{t('filters.labels.rarity')}
|
||||
</DropdownMenu.Label>
|
||||
{Array.from(Array(rarities.length)).map((x, i) => {
|
||||
return (
|
||||
<SearchFilterCheckboxItem
|
||||
key={rarities[i]}
|
||||
onCheckedChange={handleRarityChange}
|
||||
checked={rarityState[rarities[i]].checked}
|
||||
valueKey={rarities[i]}
|
||||
>
|
||||
{t(`rarities.${rarities[i]}`)}
|
||||
</SearchFilterCheckboxItem>
|
||||
)
|
||||
})}
|
||||
</SearchFilter>
|
||||
|
||||
<SearchFilter
|
||||
label={t('filters.labels.element')}
|
||||
numSelected={
|
||||
Object.values(elementState)
|
||||
.map((x) => x.checked)
|
||||
.filter(Boolean).length
|
||||
}
|
||||
open={elementMenu}
|
||||
onOpenChange={elementMenuOpened}
|
||||
>
|
||||
<DropdownMenu.Label className="Label">
|
||||
{t('filters.labels.element')}
|
||||
</DropdownMenu.Label>
|
||||
{Array.from(Array(elements.length)).map((x, i) => {
|
||||
return (
|
||||
<SearchFilterCheckboxItem
|
||||
key={elements[i]}
|
||||
onCheckedChange={handleElementChange}
|
||||
checked={elementState[elements[i]].checked}
|
||||
valueKey={elements[i]}
|
||||
>
|
||||
{t(`elements.${elements[i]}`)}
|
||||
</SearchFilterCheckboxItem>
|
||||
)
|
||||
})}
|
||||
</SearchFilter>
|
||||
|
||||
{renderProficiencyFilter(1)}
|
||||
{renderProficiencyFilter(2)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CharacterSearchFilterBar
|
||||
|
|
|
|||
|
|
@ -1,79 +1,78 @@
|
|||
.CharacterUnit {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: calc($unit / 2);
|
||||
min-height: 320px;
|
||||
max-width: 200px;
|
||||
margin-bottom: $unit * 4;
|
||||
|
||||
&.editable .CharacterImage:hover {
|
||||
border: $hover-stroke;
|
||||
box-shadow: $hover-shadow;
|
||||
cursor: pointer;
|
||||
transform: $scale-tall;
|
||||
}
|
||||
|
||||
&.filled h3 {
|
||||
display: block;
|
||||
}
|
||||
|
||||
&.filled ul {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: calc($unit / 2);
|
||||
min-height: 320px;
|
||||
max-width: 200px;
|
||||
margin-bottom: $unit * 4;
|
||||
}
|
||||
|
||||
&.editable .CharacterImage:hover {
|
||||
border: $hover-stroke;
|
||||
box-shadow: $hover-shadow;
|
||||
cursor: pointer;
|
||||
transform: $scale-tall;
|
||||
h3,
|
||||
ul {
|
||||
display: none;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: var(--text-primary);
|
||||
font-size: $font-regular;
|
||||
font-weight: $normal;
|
||||
line-height: 1.1;
|
||||
margin: 0;
|
||||
max-width: 131px;
|
||||
text-align: center;
|
||||
word-wrap: normal;
|
||||
}
|
||||
|
||||
img {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.CharacterImage {
|
||||
aspect-ratio: 131 / 273;
|
||||
background: var(--card-bg);
|
||||
border: 1px solid rgba(0, 0, 0, 0);
|
||||
border-radius: $unit;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
transition: all 0.18s ease-in-out;
|
||||
height: auto;
|
||||
width: 131px;
|
||||
|
||||
@media (max-width: $medium-screen) {
|
||||
width: 17vw;
|
||||
}
|
||||
|
||||
&.filled h3 {
|
||||
display: block;
|
||||
&:hover .icon svg {
|
||||
fill: var(--icon-secondary-hover);
|
||||
}
|
||||
|
||||
&.filled ul {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
h3,
|
||||
ul {
|
||||
display: none;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: #333;
|
||||
font-size: $font-regular;
|
||||
font-weight: $normal;
|
||||
line-height: 1.1;
|
||||
margin: 0;
|
||||
max-width: 131px;
|
||||
text-align: center;
|
||||
word-wrap: normal;
|
||||
}
|
||||
|
||||
img {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
|
||||
.CharacterImage {
|
||||
aspect-ratio: 131 / 273;
|
||||
background: white;
|
||||
border: 1px solid rgba(0, 0, 0, 0);
|
||||
border-radius: $unit;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
transition: all 0.18s ease-in-out;
|
||||
height: auto;
|
||||
width: 131px;
|
||||
|
||||
@media (max-width: $medium-screen) {
|
||||
width: 17vw;
|
||||
}
|
||||
|
||||
&:hover .icon svg {
|
||||
color: $grey-40;
|
||||
}
|
||||
|
||||
.icon {
|
||||
position: absolute;
|
||||
height: $unit * 3;
|
||||
width: $unit * 3;
|
||||
z-index: 1;
|
||||
|
||||
svg {
|
||||
fill: $grey-70;
|
||||
}
|
||||
}
|
||||
.icon {
|
||||
position: absolute;
|
||||
height: $unit * 3;
|
||||
width: $unit * 3;
|
||||
z-index: 1;
|
||||
|
||||
svg {
|
||||
fill: var(--icon-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,19 @@
|
|||
import React, { useEffect, useState } from "react"
|
||||
import { useRouter } from "next/router"
|
||||
import { useSnapshot } from "valtio"
|
||||
import { useTranslation } from "next-i18next"
|
||||
import classnames from "classnames"
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import classnames from 'classnames'
|
||||
|
||||
import { appState } from "~utils/appState"
|
||||
import { appState } from '~utils/appState'
|
||||
|
||||
import CharacterHovercard from "~components/CharacterHovercard"
|
||||
import SearchModal from "~components/SearchModal"
|
||||
import UncapIndicator from "~components/UncapIndicator"
|
||||
import PlusIcon from "~public/icons/Add.svg"
|
||||
import CharacterHovercard from '~components/CharacterHovercard'
|
||||
import SearchModal from '~components/SearchModal'
|
||||
import UncapIndicator from '~components/UncapIndicator'
|
||||
import PlusIcon from '~public/icons/Add.svg'
|
||||
|
||||
import type { SearchableObject } from "~types"
|
||||
import type { SearchableObject } from '~types'
|
||||
|
||||
import "./index.scss"
|
||||
import './index.scss'
|
||||
|
||||
interface Props {
|
||||
gridCharacter?: GridCharacter
|
||||
|
|
@ -24,15 +24,15 @@ interface Props {
|
|||
}
|
||||
|
||||
const CharacterUnit = (props: Props) => {
|
||||
const { t } = useTranslation("common")
|
||||
const { t } = useTranslation('common')
|
||||
|
||||
const { party, grid } = useSnapshot(appState)
|
||||
|
||||
const router = useRouter()
|
||||
const locale =
|
||||
router.locale && ["en", "ja"].includes(router.locale) ? router.locale : "en"
|
||||
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
|
||||
|
||||
const [imageUrl, setImageUrl] = useState("")
|
||||
const [imageUrl, setImageUrl] = useState('')
|
||||
|
||||
const classes = classnames({
|
||||
CharacterUnit: true,
|
||||
|
|
@ -48,19 +48,19 @@ const CharacterUnit = (props: Props) => {
|
|||
})
|
||||
|
||||
function generateImageUrl() {
|
||||
let imgSrc = ""
|
||||
let imgSrc = ''
|
||||
|
||||
if (props.gridCharacter) {
|
||||
const character = props.gridCharacter.object!
|
||||
|
||||
// Change the image based on the uncap level
|
||||
let suffix = "01"
|
||||
if (props.gridCharacter.uncap_level == 6) suffix = "04"
|
||||
else if (props.gridCharacter.uncap_level == 5) suffix = "03"
|
||||
else if (props.gridCharacter.uncap_level > 2) suffix = "02"
|
||||
let suffix = '01'
|
||||
if (props.gridCharacter.uncap_level == 6) suffix = '04'
|
||||
else if (props.gridCharacter.uncap_level == 5) suffix = '03'
|
||||
else if (props.gridCharacter.uncap_level > 2) suffix = '02'
|
||||
|
||||
// Special casing for Lyria (and Young Cat eventually)
|
||||
if (props.gridCharacter.object.granblue_id === "3030182000") {
|
||||
if (props.gridCharacter.object.granblue_id === '3030182000') {
|
||||
let element = 1
|
||||
if (grid.weapons.mainWeapon && grid.weapons.mainWeapon.element) {
|
||||
element = grid.weapons.mainWeapon.element
|
||||
|
|
@ -90,14 +90,14 @@ const CharacterUnit = (props: Props) => {
|
|||
<PlusIcon />
|
||||
</span>
|
||||
) : (
|
||||
""
|
||||
''
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
const editableImage = (
|
||||
<SearchModal
|
||||
placeholderText={t("search.placeholders.character")}
|
||||
placeholderText={t('search.placeholders.character')}
|
||||
fromPosition={props.position}
|
||||
object="characters"
|
||||
send={props.updateObject}
|
||||
|
|
@ -119,7 +119,7 @@ const CharacterUnit = (props: Props) => {
|
|||
special={character.special}
|
||||
/>
|
||||
) : (
|
||||
""
|
||||
''
|
||||
)}
|
||||
<h3 className="CharacterName">{character?.name[locale]}</h3>
|
||||
</div>
|
||||
|
|
|
|||
87
components/Dialog/index.scss
Normal file
87
components/Dialog/index.scss
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
.Dialog {
|
||||
$multiplier: 4;
|
||||
|
||||
animation: 0.5s cubic-bezier(0.16, 1, 0.3, 1) 0s 1 normal none running
|
||||
openModal;
|
||||
background: var(--dialog-bg);
|
||||
border-radius: $card-corner;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit * $multiplier;
|
||||
height: auto;
|
||||
min-width: $unit * 48;
|
||||
min-height: $unit-12x;
|
||||
padding: $unit * $multiplier;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 21;
|
||||
|
||||
.DialogHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $unit;
|
||||
|
||||
.left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
gap: $unit;
|
||||
|
||||
p {
|
||||
font-size: $font-small;
|
||||
line-height: 1.25;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.DialogClose {
|
||||
background: transparent;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
|
||||
svg {
|
||||
fill: $error;
|
||||
}
|
||||
}
|
||||
|
||||
svg {
|
||||
fill: $grey-50;
|
||||
float: right;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.DialogTitle {
|
||||
color: var(--text-primary);
|
||||
font-size: $font-xlarge;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.DialogTop {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
gap: calc($unit / 2);
|
||||
|
||||
.SubTitle {
|
||||
color: var(--text-secondary);
|
||||
font-size: $font-small;
|
||||
font-weight: $medium;
|
||||
}
|
||||
}
|
||||
|
||||
.DialogDescription {
|
||||
color: var(--text-secondary);
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
39
components/Dialog/index.tsx
Normal file
39
components/Dialog/index.tsx
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import React from 'react'
|
||||
import * as DialogPrimitive from '@radix-ui/react-dialog'
|
||||
import classNames from 'classnames'
|
||||
|
||||
import './index.scss'
|
||||
|
||||
interface Props
|
||||
extends React.DetailedHTMLProps<
|
||||
React.DialogHTMLAttributes<HTMLDivElement>,
|
||||
HTMLDivElement
|
||||
> {}
|
||||
|
||||
export const DialogContent = React.forwardRef<HTMLDivElement, Props>(
|
||||
function dialog({ children, ...props }, forwardedRef) {
|
||||
const classes = classNames(
|
||||
{
|
||||
Dialog: true,
|
||||
},
|
||||
props.className
|
||||
)
|
||||
|
||||
return (
|
||||
<DialogPrimitive.Portal>
|
||||
<DialogPrimitive.Overlay className="Overlay" />
|
||||
<DialogPrimitive.Content
|
||||
className={classes}
|
||||
{...props}
|
||||
ref={forwardedRef}
|
||||
>
|
||||
{children}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
export const Dialog = DialogPrimitive.Root
|
||||
export const DialogTrigger = DialogPrimitive.Trigger
|
||||
export const DialogClose = DialogPrimitive.Close
|
||||
|
|
@ -1,64 +1,65 @@
|
|||
.ToggleGroup {
|
||||
$height: 36px;
|
||||
$height: 36px;
|
||||
|
||||
border: 1px solid rgba(0, 0, 0, 0.14);
|
||||
border-radius: $height;
|
||||
display: flex;
|
||||
height: $height;
|
||||
gap: calc($unit / 4);
|
||||
padding: calc($unit / 2);
|
||||
border: 1px solid rgba(0, 0, 0, 0.14);
|
||||
border-radius: $height;
|
||||
display: flex;
|
||||
height: $height;
|
||||
gap: calc($unit / 4);
|
||||
padding: calc($unit / 2);
|
||||
|
||||
.ToggleItem {
|
||||
background: white;
|
||||
border: none;
|
||||
border-radius: 18px;
|
||||
color: $grey-40;
|
||||
flex-grow: 1;
|
||||
font-size: $font-regular;
|
||||
padding: ($unit) $unit * 2;
|
||||
.ToggleItem {
|
||||
background: $grey-100;
|
||||
border: none;
|
||||
border-radius: 18px;
|
||||
color: $grey-50;
|
||||
flex-grow: 1;
|
||||
font-size: $font-regular;
|
||||
padding: ($unit) $unit * 2;
|
||||
|
||||
&.ja {
|
||||
padding-top: 6px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:hover, &[data-state="on"] {
|
||||
background:$grey-80;
|
||||
color: $grey-00;
|
||||
|
||||
&.fire {
|
||||
background: $fire-bg-light;
|
||||
color: $fire-text-dark;
|
||||
}
|
||||
|
||||
&.water {
|
||||
background: $water-bg-light;
|
||||
color: $water-text-dark;
|
||||
}
|
||||
|
||||
&.earth {
|
||||
background: $earth-bg-light;
|
||||
color: $earth-text-dark;
|
||||
}
|
||||
|
||||
&.wind {
|
||||
background: $wind-bg-light;
|
||||
color: $wind-text-dark;
|
||||
}
|
||||
|
||||
&.dark {
|
||||
background: $dark-bg-light;
|
||||
color: $dark-text-dark;
|
||||
}
|
||||
|
||||
&.light {
|
||||
background: $light-bg-light;
|
||||
color: $light-text-dark;
|
||||
}
|
||||
}
|
||||
&.ja {
|
||||
padding-top: 6px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&[data-state='on'] {
|
||||
background: $grey-80;
|
||||
color: $grey-15;
|
||||
|
||||
&.fire {
|
||||
background: $fire-bg-20;
|
||||
color: $fire-text-10;
|
||||
}
|
||||
|
||||
&.water {
|
||||
background: $water-bg-20;
|
||||
color: $water-text-10;
|
||||
}
|
||||
|
||||
&.earth {
|
||||
background: $earth-bg-20;
|
||||
color: $earth-text-10;
|
||||
}
|
||||
|
||||
&.wind {
|
||||
background: $wind-bg-20;
|
||||
color: $wind-text-10;
|
||||
}
|
||||
|
||||
&.dark {
|
||||
background: $dark-bg-10;
|
||||
color: $dark-text-10;
|
||||
}
|
||||
|
||||
&.light {
|
||||
background: $light-bg-20;
|
||||
color: $light-text-10;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,40 +7,75 @@ import * as ToggleGroup from '@radix-ui/react-toggle-group'
|
|||
import './index.scss'
|
||||
|
||||
interface Props {
|
||||
currentElement: number
|
||||
sendValue: (value: string) => void
|
||||
currentElement: number
|
||||
sendValue: (value: string) => void
|
||||
}
|
||||
|
||||
const ElementToggle = (props: Props) => {
|
||||
const router = useRouter()
|
||||
const { t } = useTranslation('common')
|
||||
const locale = (router.locale && ['en', 'ja'].includes(router.locale)) ? router.locale : 'en'
|
||||
const router = useRouter()
|
||||
const { t } = useTranslation('common')
|
||||
const locale =
|
||||
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
|
||||
|
||||
return (
|
||||
<ToggleGroup.Root className="ToggleGroup" type="single" defaultValue={`${props.currentElement}`} aria-label="Element" onValueChange={props.sendValue}>
|
||||
<ToggleGroup.Item className={`ToggleItem ${locale}`} value="0" aria-label="null">
|
||||
{t('elements.null')}
|
||||
</ToggleGroup.Item>
|
||||
<ToggleGroup.Item className={`ToggleItem wind ${locale}`} value="1" aria-label="wind">
|
||||
{t('elements.wind')}
|
||||
</ToggleGroup.Item>
|
||||
<ToggleGroup.Item className={`ToggleItem fire ${locale}`} value="2" aria-label="fire">
|
||||
{t('elements.fire')}
|
||||
</ToggleGroup.Item>
|
||||
<ToggleGroup.Item className={`ToggleItem water ${locale}`} value="3" aria-label="water">
|
||||
{t('elements.water')}
|
||||
</ToggleGroup.Item>
|
||||
<ToggleGroup.Item className={`ToggleItem earth ${locale}`} value="4" aria-label="earth">
|
||||
{t('elements.earth')}
|
||||
</ToggleGroup.Item>
|
||||
<ToggleGroup.Item className={`ToggleItem dark ${locale}`} value="5" aria-label="dark">
|
||||
{t('elements.dark')}
|
||||
</ToggleGroup.Item>
|
||||
<ToggleGroup.Item className={`ToggleItem light ${locale}`} value="6" aria-label="light">
|
||||
{t('elements.light')}
|
||||
</ToggleGroup.Item>
|
||||
</ToggleGroup.Root>
|
||||
)
|
||||
return (
|
||||
<ToggleGroup.Root
|
||||
className="ToggleGroup"
|
||||
type="single"
|
||||
defaultValue={`${props.currentElement}`}
|
||||
aria-label="Element"
|
||||
onValueChange={props.sendValue}
|
||||
>
|
||||
<ToggleGroup.Item
|
||||
className={`ToggleItem ${locale}`}
|
||||
value="0"
|
||||
aria-label="null"
|
||||
>
|
||||
{t('elements.null')}
|
||||
</ToggleGroup.Item>
|
||||
<ToggleGroup.Item
|
||||
className={`ToggleItem wind ${locale}`}
|
||||
value="1"
|
||||
aria-label="wind"
|
||||
>
|
||||
{t('elements.wind')}
|
||||
</ToggleGroup.Item>
|
||||
<ToggleGroup.Item
|
||||
className={`ToggleItem fire ${locale}`}
|
||||
value="2"
|
||||
aria-label="fire"
|
||||
>
|
||||
{t('elements.fire')}
|
||||
</ToggleGroup.Item>
|
||||
<ToggleGroup.Item
|
||||
className={`ToggleItem water ${locale}`}
|
||||
value="3"
|
||||
aria-label="water"
|
||||
>
|
||||
{t('elements.water')}
|
||||
</ToggleGroup.Item>
|
||||
<ToggleGroup.Item
|
||||
className={`ToggleItem earth ${locale}`}
|
||||
value="4"
|
||||
aria-label="earth"
|
||||
>
|
||||
{t('elements.earth')}
|
||||
</ToggleGroup.Item>
|
||||
<ToggleGroup.Item
|
||||
className={`ToggleItem dark ${locale}`}
|
||||
value="5"
|
||||
aria-label="dark"
|
||||
>
|
||||
{t('elements.dark')}
|
||||
</ToggleGroup.Item>
|
||||
<ToggleGroup.Item
|
||||
className={`ToggleItem light ${locale}`}
|
||||
value="6"
|
||||
aria-label="light"
|
||||
>
|
||||
{t('elements.light')}
|
||||
</ToggleGroup.Item>
|
||||
</ToggleGroup.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export default ElementToggle
|
||||
export default ElementToggle
|
||||
|
|
|
|||
|
|
@ -1,55 +1,55 @@
|
|||
#ExtraSummons {
|
||||
background: #FFEBD9;
|
||||
border-radius: 8px;
|
||||
box-sizing: border-box;
|
||||
background: var(--subaura-orange-bg);
|
||||
border-radius: 8px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 20px auto;
|
||||
max-width: 727px;
|
||||
padding: 16px 16px 16px 0;
|
||||
position: relative;
|
||||
left: 9px;
|
||||
|
||||
@media (max-width: $medium-screen) {
|
||||
left: auto;
|
||||
max-width: auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
& > span {
|
||||
color: var(--subaura-orange-text);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 20px auto;
|
||||
max-width: 727px;
|
||||
padding: 16px 16px 16px 0;
|
||||
position: relative;
|
||||
left: 9px;
|
||||
line-height: 1.2;
|
||||
font-weight: 500;
|
||||
margin-right: 16px;
|
||||
text-align: center;
|
||||
width: 387px;
|
||||
}
|
||||
|
||||
@media (max-width: $medium-screen) {
|
||||
left: auto;
|
||||
max-width: auto;
|
||||
width: 100%;
|
||||
#grid_summons {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
grid-column-gap: $unit * 2;
|
||||
grid-template-rows: 1fr;
|
||||
grid-row-gap: $unit * 3;
|
||||
|
||||
& > li {
|
||||
list-style: none;
|
||||
min-height: 0;
|
||||
|
||||
.SummonUnit {
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& > span {
|
||||
color: #825B39;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
line-height: 1.2;
|
||||
font-weight: 500;
|
||||
margin-right: 16px;
|
||||
text-align: center;
|
||||
width: 387px;
|
||||
}
|
||||
.SummonUnit .SummonImage {
|
||||
background: var(--subaura-orange-card-bg);
|
||||
}
|
||||
|
||||
#grid_summons {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
grid-column-gap: $unit * 2;
|
||||
grid-template-rows: 1fr;
|
||||
grid-row-gap: $unit * 3;
|
||||
|
||||
& > li {
|
||||
list-style: none;
|
||||
min-height: 0;
|
||||
|
||||
.SummonUnit {
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.SummonUnit .SummonImage {
|
||||
background: #facea7;
|
||||
}
|
||||
|
||||
.SummonUnit .SummonImage .icon svg {
|
||||
fill: #a8703f;
|
||||
}
|
||||
}
|
||||
.SummonUnit .SummonImage .icon svg {
|
||||
fill: var(--subaura-orange-secondary);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import React from "react"
|
||||
import { useTranslation } from "next-i18next"
|
||||
import SummonUnit from "~components/SummonUnit"
|
||||
import { SearchableObject } from "~types"
|
||||
import "./index.scss"
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import SummonUnit from '~components/SummonUnit'
|
||||
import { SearchableObject } from '~types'
|
||||
import './index.scss'
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
|
|
@ -18,11 +18,11 @@ interface Props {
|
|||
const ExtraSummons = (props: Props) => {
|
||||
const numSummons: number = 2
|
||||
|
||||
const { t } = useTranslation("common")
|
||||
const { t } = useTranslation('common')
|
||||
|
||||
return (
|
||||
<div id="ExtraSummons">
|
||||
<span>{t("summons.subaura")}</span>
|
||||
<span>{t('summons.subaura')}</span>
|
||||
<ul id="grid_summons">
|
||||
{Array.from(Array(numSummons)).map((x, i) => {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,47 +1,47 @@
|
|||
#ExtraGrid {
|
||||
background: #ECEBFF;
|
||||
border-radius: 8px;
|
||||
box-sizing: border-box;
|
||||
background: var(--extra-purple-bg);
|
||||
border-radius: 8px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 20px auto;
|
||||
max-width: 766px;
|
||||
padding: 16px 16px 16px 0;
|
||||
position: relative;
|
||||
left: 8px;
|
||||
|
||||
@media (max-width: $medium-screen) {
|
||||
left: auto;
|
||||
max-width: auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
& > span {
|
||||
color: var(--extra-purple-text);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
justify-content: center;
|
||||
margin: 20px auto;
|
||||
max-width: 766px;
|
||||
padding: 16px 16px 16px 0;
|
||||
position: relative;
|
||||
left: 8px;
|
||||
line-height: 1.2;
|
||||
font-weight: 500;
|
||||
margin-right: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (max-width: $medium-screen) {
|
||||
left: auto;
|
||||
max-width: auto;
|
||||
width: 100%;
|
||||
}
|
||||
.grid_weapons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
max-width: 528px;
|
||||
}
|
||||
|
||||
& > span {
|
||||
color: #4F3C79;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
justify-content: center;
|
||||
line-height: 1.2;
|
||||
font-weight: 500;
|
||||
margin-right: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
.WeaponUnit .WeaponImage {
|
||||
background: var(--extra-purple-card-bg);
|
||||
}
|
||||
|
||||
.grid_weapons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
max-width: 528px;
|
||||
}
|
||||
|
||||
.WeaponUnit .WeaponImage {
|
||||
background: #D5D3F6;
|
||||
}
|
||||
|
||||
.WeaponUnit .WeaponImage .icon svg {
|
||||
fill: #8F8AC6;
|
||||
}
|
||||
}
|
||||
.WeaponUnit .WeaponImage .icon svg {
|
||||
fill: var(--extra-purple-secondary);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import React from "react"
|
||||
import { useTranslation } from "next-i18next"
|
||||
import WeaponUnit from "~components/WeaponUnit"
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import WeaponUnit from '~components/WeaponUnit'
|
||||
|
||||
import type { SearchableObject } from "~types"
|
||||
import type { SearchableObject } from '~types'
|
||||
|
||||
import "./index.scss"
|
||||
import './index.scss'
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
|
|
@ -18,17 +18,17 @@ interface Props {
|
|||
|
||||
const ExtraWeapons = (props: Props) => {
|
||||
const numWeapons: number = 3
|
||||
const { t } = useTranslation("common")
|
||||
const { t } = useTranslation('common')
|
||||
|
||||
return (
|
||||
<div id="ExtraGrid">
|
||||
<span>{t("extra_weapons")}</span>
|
||||
<span>{t('extra_weapons')}</span>
|
||||
<ul className="grid_weapons">
|
||||
{Array.from(Array(numWeapons)).map((x, i) => {
|
||||
return (
|
||||
<li key={`grid_unit_${i}`}>
|
||||
<WeaponUnit
|
||||
editable={props.editable}
|
||||
editable={i < 2 ? props.editable : false}
|
||||
position={props.offset + i}
|
||||
unitType={1}
|
||||
gridWeapon={props.grid[props.offset + i]}
|
||||
|
|
|
|||
|
|
@ -1,33 +0,0 @@
|
|||
.Fieldset {
|
||||
border: none;
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
margin: 0 0 $unit 0;
|
||||
|
||||
.Input {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
border: none;
|
||||
|
||||
background-color: white;
|
||||
border-radius: 6px;
|
||||
box-sizing: border-box;
|
||||
color: $grey-00;
|
||||
display: block;
|
||||
font-size: $font-regular;
|
||||
padding: 12px 16px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.InputError {
|
||||
color: $error;
|
||||
font-size: $font-tiny;
|
||||
margin: $unit 0;
|
||||
padding: calc($unit / 2) ($unit * 2);
|
||||
}
|
||||
}
|
||||
|
||||
::placeholder { /* Chrome, Firefox, Opera, Safari 10.1+ */
|
||||
color: #a9a9a9 !important;
|
||||
opacity: 1; /* Firefox */
|
||||
}
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
import React from 'react'
|
||||
import './index.scss'
|
||||
|
||||
interface Props {
|
||||
fieldName: string
|
||||
placeholder: string
|
||||
value?: string
|
||||
error: string
|
||||
onBlur?: (event: React.ChangeEvent<HTMLInputElement>) => void
|
||||
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void
|
||||
}
|
||||
|
||||
const Fieldset = React.forwardRef<HTMLInputElement, Props>(function fieldSet(props, ref) {
|
||||
const fieldType = (['password', 'confirm_password'].includes(props.fieldName)) ? 'password' : 'text'
|
||||
|
||||
return (
|
||||
<fieldset className="Fieldset">
|
||||
<input
|
||||
autoComplete="off"
|
||||
className="Input"
|
||||
type={fieldType}
|
||||
name={props.fieldName}
|
||||
placeholder={props.placeholder}
|
||||
defaultValue={props.value || ''}
|
||||
onBlur={props.onBlur}
|
||||
onChange={props.onChange}
|
||||
ref={ref}
|
||||
formNoValidate
|
||||
/>
|
||||
{
|
||||
props.error.length > 0 &&
|
||||
<p className='InputError'>{props.error}</p>
|
||||
}
|
||||
</fieldset>
|
||||
)
|
||||
})
|
||||
|
||||
export default Fieldset
|
||||
|
|
@ -1,63 +1,76 @@
|
|||
.FilterBar {
|
||||
align-items: center;
|
||||
background: var(--bar-bg);
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: $unit * 2;
|
||||
margin: 0 auto;
|
||||
margin-top: 7px; // Line up with HeaderMenu
|
||||
padding: $unit * 2;
|
||||
position: sticky;
|
||||
transition: box-shadow 0.24s ease-in-out;
|
||||
top: $unit * 4;
|
||||
width: 966px;
|
||||
|
||||
&.shadow {
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.14);
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: var(--text-primary);
|
||||
font-size: $font-regular;
|
||||
font-weight: $normal;
|
||||
flex-grow: 1;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
select,
|
||||
.SelectTrigger {
|
||||
// background: url("/icons/Arrow.svg"), $grey-90;
|
||||
// background-repeat: no-repeat;
|
||||
// background-position-y: center;
|
||||
// background-position-x: 95%;
|
||||
// background-size: $unit * 1.5;
|
||||
background-color: var(--select-contained-bg);
|
||||
color: $grey-55;
|
||||
font-size: $font-small;
|
||||
margin: 0;
|
||||
max-width: 200px;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--select-contained-bg-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.SelectTrigger {
|
||||
width: 100%;
|
||||
|
||||
span {
|
||||
font-size: $font-small;
|
||||
}
|
||||
}
|
||||
|
||||
.UserInfo {
|
||||
align-items: center;
|
||||
background: white;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: $unit * 2;
|
||||
margin: 0 auto;
|
||||
margin-top: 7px; // Line up with HeaderMenu
|
||||
padding: $unit * 2;
|
||||
position: sticky;
|
||||
transition: box-shadow 0.24s ease-in-out;
|
||||
top: $unit * 4;
|
||||
width: 966px;
|
||||
flex-grow: 1;
|
||||
gap: $unit * 1.5;
|
||||
|
||||
&.shadow {
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.14);
|
||||
img {
|
||||
$diameter: $unit * 6;
|
||||
border-radius: $diameter / 2;
|
||||
height: $diameter;
|
||||
width: $diameter;
|
||||
|
||||
&.gran {
|
||||
background-color: #cee7fe;
|
||||
}
|
||||
|
||||
&.djeeta {
|
||||
background-color: #ffe1fe;
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: $grey-20;
|
||||
font-size: $font-regular;
|
||||
font-weight: $normal;
|
||||
flex-grow: 1;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
select {
|
||||
background: url('/icons/Arrow.svg'), $grey-90;
|
||||
background-repeat: no-repeat;
|
||||
background-position-y: center;
|
||||
background-position-x: 95%;
|
||||
background-size: $unit * 1.5;
|
||||
color: $grey-50;
|
||||
font-size: $font-small;
|
||||
margin: 0;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
|
||||
.UserInfo {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-grow: 1;
|
||||
gap: $unit * 1.5;
|
||||
|
||||
img {
|
||||
$diameter: $unit * 6;
|
||||
border-radius: $diameter / 2;
|
||||
height: $diameter;
|
||||
width: $diameter;
|
||||
|
||||
&.gran {
|
||||
background-color: #CEE7FE;
|
||||
}
|
||||
|
||||
&.djeeta {
|
||||
background-color: #FFE1FE;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,79 +1,145 @@
|
|||
import React from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import classNames from 'classnames'
|
||||
|
||||
import RaidDropdown from '~components/RaidDropdown'
|
||||
|
||||
import './index.scss'
|
||||
import Select from '~components/Select'
|
||||
import SelectItem from '~components/SelectItem'
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode
|
||||
scrolled: boolean
|
||||
children: React.ReactNode
|
||||
scrolled: boolean
|
||||
element?: number
|
||||
raidSlug?: string
|
||||
recency?: number
|
||||
onFilter: ({
|
||||
element,
|
||||
raidSlug,
|
||||
recency,
|
||||
}: {
|
||||
element?: number
|
||||
raidSlug?: string
|
||||
recency?: number
|
||||
onFilter: ({element, raidSlug, recency} : { element?: number, raidSlug?: string, recency?: number}) => void
|
||||
}) => void
|
||||
}
|
||||
|
||||
const FilterBar = (props: Props) => {
|
||||
// Set up translation
|
||||
const { t } = useTranslation('common')
|
||||
const FilterBar = (props: Props) => {
|
||||
// Set up translation
|
||||
const { t } = useTranslation('common')
|
||||
|
||||
// Set up refs for filter dropdowns
|
||||
const elementSelect = React.createRef<HTMLSelectElement>()
|
||||
const raidSelect = React.createRef<HTMLSelectElement>()
|
||||
const recencySelect = React.createRef<HTMLSelectElement>()
|
||||
const [recencyOpen, setRecencyOpen] = useState(false)
|
||||
const [elementOpen, setElementOpen] = useState(false)
|
||||
|
||||
// Set up classes object for showing shadow on scroll
|
||||
const classes = classNames({
|
||||
'FilterBar': true,
|
||||
'shadow': props.scrolled
|
||||
})
|
||||
// Set up refs for filter dropdowns
|
||||
const elementSelect = React.createRef<HTMLSelectElement>()
|
||||
const raidSelect = React.createRef<HTMLSelectElement>()
|
||||
const recencySelect = React.createRef<HTMLSelectElement>()
|
||||
|
||||
function elementSelectChanged() {
|
||||
const elementValue = (elementSelect.current) ? parseInt(elementSelect.current.value) : -1
|
||||
props.onFilter({ element: elementValue })
|
||||
}
|
||||
// Set up classes object for showing shadow on scroll
|
||||
const classes = classNames({
|
||||
FilterBar: true,
|
||||
shadow: props.scrolled,
|
||||
})
|
||||
|
||||
function recencySelectChanged() {
|
||||
const recencyValue = (recencySelect.current) ? parseInt(recencySelect.current.value) : -1
|
||||
props.onFilter({ recency: recencyValue })
|
||||
}
|
||||
function openElementSelect() {
|
||||
setElementOpen(!elementOpen)
|
||||
}
|
||||
|
||||
function raidSelectChanged(slug?: string) {
|
||||
props.onFilter({ raidSlug: slug })
|
||||
}
|
||||
function openRecencySelect() {
|
||||
setRecencyOpen(!recencyOpen)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes}>
|
||||
{props.children}
|
||||
<select onChange={elementSelectChanged} ref={elementSelect} value={props.element}>
|
||||
<option data-element="all" key={-1} value={-1}>{t('elements.full.all')}</option>
|
||||
<option data-element="null" key={0} value={0}>{t('elements.full.null')}</option>
|
||||
<option data-element="wind" key={1} value={1}>{t('elements.full.wind')}</option>
|
||||
<option data-element="fire" key={2} value={2}>{t('elements.full.fire')}</option>
|
||||
<option data-element="water" key={3} value={3}>{t('elements.full.water')}</option>
|
||||
<option data-element="earth" key={4} value={4}>{t('elements.full.earth')}</option>
|
||||
<option data-element="dark" key={5} value={5}>{t('elements.full.dark')}</option>
|
||||
<option data-element="light" key={6} value={6}>{t('elements.full.light')}</option>
|
||||
</select>
|
||||
<RaidDropdown
|
||||
currentRaid={props.raidSlug}
|
||||
showAllRaidsOption={true}
|
||||
onChange={raidSelectChanged}
|
||||
ref={raidSelect}
|
||||
/>
|
||||
<select onChange={recencySelectChanged} ref={recencySelect}>
|
||||
<option key={-1} value={-1}>{t('recency.all_time')}</option>
|
||||
<option key={86400} value={86400}>{t('recency.last_day')}</option>
|
||||
<option key={604800} value={604800}>{t('recency.last_week')}</option>
|
||||
<option key={2629746} value={2629746}>{t('recency.last_month')}</option>
|
||||
<option key={7889238} value={7889238}>{t('recency.last_3_months')}</option>
|
||||
<option key={15778476} value={15778476}>{t('recency.last_6_months')}</option>
|
||||
<option key={31556952} value={31556952}>{t('recency.last_year')}</option>
|
||||
</select>
|
||||
</div>
|
||||
)
|
||||
function elementSelectChanged(value: string) {
|
||||
const elementValue = parseInt(value)
|
||||
props.onFilter({ element: elementValue })
|
||||
}
|
||||
|
||||
function recencySelectChanged(value: string) {
|
||||
const recencyValue = parseInt(value)
|
||||
props.onFilter({ recency: recencyValue })
|
||||
}
|
||||
|
||||
function raidSelectChanged(slug?: string) {
|
||||
props.onFilter({ raidSlug: slug })
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes}>
|
||||
{props.children}
|
||||
<Select
|
||||
defaultValue={-1}
|
||||
trigger={'All elements'}
|
||||
open={elementOpen}
|
||||
onChange={elementSelectChanged}
|
||||
onClick={openElementSelect}
|
||||
>
|
||||
<SelectItem data-element="all" key={-1} value={-1}>
|
||||
{t('elements.full.all')}
|
||||
</SelectItem>
|
||||
<SelectItem data-element="null" key={0} value={0}>
|
||||
{t('elements.full.null')}
|
||||
</SelectItem>
|
||||
<SelectItem data-element="wind" key={1} value={1}>
|
||||
{t('elements.full.wind')}
|
||||
</SelectItem>
|
||||
<SelectItem data-element="fire" key={2} value={2}>
|
||||
{t('elements.full.fire')}
|
||||
</SelectItem>
|
||||
<SelectItem data-element="water" key={3} value={3}>
|
||||
{t('elements.full.water')}
|
||||
</SelectItem>
|
||||
<SelectItem data-element="earth" key={4} value={4}>
|
||||
{t('elements.full.earth')}
|
||||
</SelectItem>
|
||||
<SelectItem data-element="dark" key={5} value={5}>
|
||||
{t('elements.full.dark')}
|
||||
</SelectItem>
|
||||
<SelectItem data-element="light" key={6} value={6}>
|
||||
{t('elements.full.light')}
|
||||
</SelectItem>
|
||||
</Select>
|
||||
|
||||
<RaidDropdown
|
||||
currentRaid={props.raidSlug}
|
||||
defaultRaid="all"
|
||||
showAllRaidsOption={true}
|
||||
onChange={raidSelectChanged}
|
||||
ref={raidSelect}
|
||||
/>
|
||||
|
||||
<Select
|
||||
defaultValue={-1}
|
||||
trigger={'All time'}
|
||||
open={recencyOpen}
|
||||
onChange={recencySelectChanged}
|
||||
onClick={openRecencySelect}
|
||||
>
|
||||
<SelectItem key={-1} value={-1}>
|
||||
{t('recency.all_time')}
|
||||
</SelectItem>
|
||||
<SelectItem key={86400} value={86400}>
|
||||
{t('recency.last_day')}
|
||||
</SelectItem>
|
||||
<SelectItem key={604800} value={604800}>
|
||||
{t('recency.last_week')}
|
||||
</SelectItem>
|
||||
<SelectItem key={2629746} value={2629746}>
|
||||
{t('recency.last_month')}
|
||||
</SelectItem>
|
||||
<SelectItem key={7889238} value={7889238}>
|
||||
{t('recency.last_3_months')}
|
||||
</SelectItem>
|
||||
<SelectItem key={15778476} value={15778476}>
|
||||
{t('recency.last_6_months')}
|
||||
</SelectItem>
|
||||
<SelectItem key={31556952} value={31556952}>
|
||||
{t('recency.last_year')}
|
||||
</SelectItem>
|
||||
</Select>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default FilterBar
|
||||
|
|
|
|||
|
|
@ -1,148 +1,147 @@
|
|||
.GridRep {
|
||||
border-radius: 6px;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit;
|
||||
padding: $unit * 2;
|
||||
|
||||
&:hover {
|
||||
background: var(--grid-rep-hover);
|
||||
|
||||
h2,
|
||||
.Grid {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.Grid .weapon {
|
||||
box-shadow: inset 0 0 0 1px var(--grid-border-color);
|
||||
}
|
||||
}
|
||||
|
||||
.Grid {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-shrink: 0;
|
||||
|
||||
.weapon {
|
||||
background: var(--card-bg);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.grid_mainhand {
|
||||
margin-right: $unit;
|
||||
height: 139px;
|
||||
width: 66px;
|
||||
}
|
||||
|
||||
.grid_weapons {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
grid-template-rows: 1fr 1fr 1fr;
|
||||
gap: $unit;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.grid_weapon {
|
||||
float: left;
|
||||
height: 40px;
|
||||
width: 70px;
|
||||
}
|
||||
|
||||
.grid_mainhand img[src*='jpg'],
|
||||
.grid_weapon img[src*='jpg'] {
|
||||
border-radius: 4px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.Details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit;
|
||||
padding: $unit * 2;
|
||||
gap: calc($unit / 2);
|
||||
|
||||
&:hover {
|
||||
background: white;
|
||||
h2 {
|
||||
color: var(--text-primary);
|
||||
font-size: $font-regular;
|
||||
overflow: hidden;
|
||||
padding-bottom: 1px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 258px; // Can we not do this?
|
||||
|
||||
h2, .Grid {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.Grid .weapon {
|
||||
box-shadow: inset 0 0 0 1px $grey-80;
|
||||
}
|
||||
&.empty {
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
}
|
||||
|
||||
.Grid {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-shrink: 0;
|
||||
.top {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: calc($unit / 2);
|
||||
align-items: center;
|
||||
|
||||
.weapon {
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.grid_mainhand {
|
||||
margin-right: $unit;
|
||||
height: 139px;
|
||||
width: 66px;
|
||||
}
|
||||
|
||||
.grid_weapons {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
grid-template-rows: 1fr 1fr 1fr;
|
||||
gap: $unit;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.grid_weapon {
|
||||
float: left;
|
||||
height: 40px;
|
||||
width: 70px;
|
||||
}
|
||||
|
||||
.grid_mainhand img[src*="jpg"],
|
||||
.grid_weapon img[src*="jpg"] {
|
||||
border-radius: 4px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.Details {
|
||||
.info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
gap: calc($unit / 2);
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: $grey-00;
|
||||
font-size: $font-regular;
|
||||
overflow: hidden;
|
||||
padding-bottom: 1px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 258px; // Can we not do this?
|
||||
|
||||
&.empty {
|
||||
color: $grey-50;
|
||||
}
|
||||
}
|
||||
|
||||
.top {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: calc($unit / 2);
|
||||
align-items: center;
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
gap: calc($unit / 2);
|
||||
}
|
||||
|
||||
button svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
button:hover,
|
||||
button.Active {
|
||||
background: $grey-90;
|
||||
}
|
||||
}
|
||||
|
||||
.bottom {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.raid, .user, time {
|
||||
color: $grey-50;
|
||||
font-size: $font-small;
|
||||
}
|
||||
|
||||
.raid, .user {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.raid {
|
||||
margin-bottom: calc($unit / 2);
|
||||
}
|
||||
|
||||
.user {
|
||||
display: flex;
|
||||
gap: calc($unit / 2);
|
||||
align-items: center;
|
||||
|
||||
|
||||
img, .no-user {
|
||||
$diameter: 18px;
|
||||
|
||||
border-radius: calc($diameter / 2);
|
||||
height: $diameter;
|
||||
width: $diameter;
|
||||
}
|
||||
|
||||
img.gran {
|
||||
background-color: #CEE7FE;
|
||||
}
|
||||
|
||||
img.djeeta {
|
||||
background-color: #FFE1FE;
|
||||
}
|
||||
|
||||
.no-user {
|
||||
background: $grey-80;
|
||||
}
|
||||
}
|
||||
button svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.bottom {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.raid,
|
||||
.user,
|
||||
time {
|
||||
color: $grey-55;
|
||||
font-size: $font-small;
|
||||
}
|
||||
|
||||
.raid,
|
||||
.user {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.raid {
|
||||
margin-bottom: calc($unit / 2);
|
||||
}
|
||||
|
||||
.user {
|
||||
display: flex;
|
||||
gap: calc($unit / 2);
|
||||
align-items: center;
|
||||
|
||||
img,
|
||||
.no-user {
|
||||
$diameter: 18px;
|
||||
|
||||
border-radius: calc($diameter / 2);
|
||||
height: $diameter;
|
||||
width: $diameter;
|
||||
}
|
||||
|
||||
img.gran {
|
||||
background-color: #cee7fe;
|
||||
}
|
||||
|
||||
img.djeeta {
|
||||
background-color: #ffe1fe;
|
||||
}
|
||||
|
||||
.no-user {
|
||||
background: $grey-80;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,17 @@
|
|||
import React, { useEffect, useState } from "react"
|
||||
import { useRouter } from "next/router"
|
||||
import { useSnapshot } from "valtio"
|
||||
import { useTranslation } from "next-i18next"
|
||||
import classNames from "classnames"
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import classNames from 'classnames'
|
||||
|
||||
import { accountState } from "~utils/accountState"
|
||||
import { formatTimeAgo } from "~utils/timeAgo"
|
||||
import { accountState } from '~utils/accountState'
|
||||
import { formatTimeAgo } from '~utils/timeAgo'
|
||||
|
||||
import Button from "~components/Button"
|
||||
import { ButtonType } from "~utils/enums"
|
||||
import Button from '~components/Button'
|
||||
|
||||
import "./index.scss"
|
||||
import SaveIcon from '~public/icons/Save.svg'
|
||||
|
||||
import './index.scss'
|
||||
|
||||
interface Props {
|
||||
shortcode: string
|
||||
|
|
@ -32,9 +33,9 @@ const GridRep = (props: Props) => {
|
|||
const { account } = useSnapshot(accountState)
|
||||
|
||||
const router = useRouter()
|
||||
const { t } = useTranslation("common")
|
||||
const { t } = useTranslation('common')
|
||||
const locale =
|
||||
router.locale && ["en", "ja"].includes(router.locale) ? router.locale : "en"
|
||||
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
|
||||
|
||||
const [mainhand, setMainhand] = useState<Weapon>()
|
||||
const [weapons, setWeapons] = useState<GridArray<Weapon>>({})
|
||||
|
|
@ -75,7 +76,7 @@ const GridRep = (props: Props) => {
|
|||
}
|
||||
|
||||
function generateMainhandImage() {
|
||||
let url = ""
|
||||
let url = ''
|
||||
|
||||
if (mainhand) {
|
||||
if (mainhand.element == 0 && props.grid[0].element) {
|
||||
|
|
@ -88,12 +89,12 @@ const GridRep = (props: Props) => {
|
|||
return mainhand && props.grid[0] ? (
|
||||
<img alt={mainhand.name[locale]} src={url} />
|
||||
) : (
|
||||
""
|
||||
''
|
||||
)
|
||||
}
|
||||
|
||||
function generateGridImage(position: number) {
|
||||
let url = ""
|
||||
let url = ''
|
||||
|
||||
const weapon = weapons[position]
|
||||
const gridWeapon = grid[position]
|
||||
|
|
@ -109,7 +110,7 @@ const GridRep = (props: Props) => {
|
|||
return weapons[position] ? (
|
||||
<img alt={weapons[position]?.name[locale]} src={url} />
|
||||
) : (
|
||||
""
|
||||
''
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -134,11 +135,11 @@ const GridRep = (props: Props) => {
|
|||
const details = (
|
||||
<div className="Details">
|
||||
<h2 className={titleClass} onClick={navigate}>
|
||||
{props.name ? props.name : t("no_title")}
|
||||
{props.name ? props.name : t('no_title')}
|
||||
</h2>
|
||||
<div className="bottom">
|
||||
<div className={raidClass}>
|
||||
{props.raid ? props.raid.name[locale] : t("no_raid")}
|
||||
{props.raid ? props.raid.name[locale] : t('no_raid')}
|
||||
</div>
|
||||
<time className="last-updated" dateTime={props.createdAt.toISOString()}>
|
||||
{formatTimeAgo(props.createdAt, locale)}
|
||||
|
|
@ -152,29 +153,31 @@ const GridRep = (props: Props) => {
|
|||
<div className="top">
|
||||
<div className="info">
|
||||
<h2 className={titleClass} onClick={navigate}>
|
||||
{props.name ? props.name : t("no_title")}
|
||||
{props.name ? props.name : t('no_title')}
|
||||
</h2>
|
||||
<div className={raidClass}>
|
||||
{props.raid ? props.raid.name[locale] : t("no_raid")}
|
||||
{props.raid ? props.raid.name[locale] : t('no_raid')}
|
||||
</div>
|
||||
</div>
|
||||
{account.authorized &&
|
||||
((props.user && account.user && account.user.id !== props.user.id) ||
|
||||
!props.user) ? (
|
||||
<Button
|
||||
className="Save"
|
||||
accessoryIcon={<SaveIcon class="stroke" />}
|
||||
active={props.favorited}
|
||||
icon="save"
|
||||
type={ButtonType.IconOnly}
|
||||
contained={true}
|
||||
size="small"
|
||||
onClick={sendSaveData}
|
||||
/>
|
||||
) : (
|
||||
""
|
||||
''
|
||||
)}
|
||||
</div>
|
||||
<div className="bottom">
|
||||
<div className={userClass}>
|
||||
{userImage()}
|
||||
{props.user ? props.user.username : t("no_user")}
|
||||
{props.user ? props.user.username : t('no_user')}
|
||||
</div>
|
||||
<time className="last-updated" dateTime={props.createdAt.toISOString()}>
|
||||
{formatTimeAgo(props.createdAt, locale)}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import classNames from "classnames"
|
||||
import React from "react"
|
||||
import classNames from 'classnames'
|
||||
import React from 'react'
|
||||
|
||||
import "./index.scss"
|
||||
import './index.scss'
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode
|
||||
|
|
|
|||
|
|
@ -1,37 +1,37 @@
|
|||
.Header {
|
||||
display: flex;
|
||||
margin-bottom: $unit-2x;
|
||||
width: 100%;
|
||||
|
||||
&.bottom {
|
||||
position: sticky;
|
||||
bottom: $unit * 2;
|
||||
}
|
||||
|
||||
#right > div {
|
||||
display: flex;
|
||||
height: 34px;
|
||||
width: 100%;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
&.bottom {
|
||||
position: sticky;
|
||||
bottom: $unit * 2;
|
||||
.dropdown {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
padding-right: 50px;
|
||||
|
||||
.Button {
|
||||
background: var(--button-bg-hover);
|
||||
color: var(--button-text-hover);
|
||||
}
|
||||
|
||||
.Menu {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#right > div {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
padding-right: 50px;
|
||||
padding-bottom: 16px;
|
||||
|
||||
.Button {
|
||||
background: white;
|
||||
}
|
||||
|
||||
.Menu {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.push {
|
||||
margin-left: auto;
|
||||
}
|
||||
.push {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,19 +3,19 @@ import React from 'react'
|
|||
import './index.scss'
|
||||
|
||||
interface Props {
|
||||
position: 'top' | 'bottom'
|
||||
left: JSX.Element,
|
||||
right: JSX.Element
|
||||
position: 'top' | 'bottom'
|
||||
left: JSX.Element
|
||||
right: JSX.Element
|
||||
}
|
||||
|
||||
const Header = (props: Props) => {
|
||||
return (
|
||||
<nav className={`Header ${props.position}`}>
|
||||
<div id="left">{ props.left }</div>
|
||||
<div className="push" />
|
||||
<div id="right">{ props.right }</div>
|
||||
</nav>
|
||||
)
|
||||
const Header = (props: Props) => {
|
||||
return (
|
||||
<nav className={`Header ${props.position}`}>
|
||||
<div id="left">{props.left}</div>
|
||||
<div className="push" />
|
||||
<div id="right">{props.right}</div>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
export default Header
|
||||
export default Header
|
||||
|
|
|
|||
|
|
@ -1,147 +1,150 @@
|
|||
.Menu {
|
||||
background: white;
|
||||
border-radius: 6px;
|
||||
display: none;
|
||||
min-width: 220px;
|
||||
position: absolute;
|
||||
top: $unit * 5; // This shouldn't be hardcoded. How to calculate it?
|
||||
z-index: 10;
|
||||
background: var(--menu-bg);
|
||||
border-radius: 6px;
|
||||
display: none;
|
||||
min-width: 220px;
|
||||
position: absolute;
|
||||
top: $unit * 5; // This shouldn't be hardcoded. How to calculate it?
|
||||
// Also, add space that doesn't make the menu disappear if you move your mouse slowly
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.MenuItem {
|
||||
color: $grey-40;
|
||||
font-weight: $normal;
|
||||
color: var(--text-tertiary);
|
||||
font-weight: $normal;
|
||||
|
||||
&:hover:not(.disabled) {
|
||||
background: $grey-100;
|
||||
color: $grey-00;
|
||||
cursor: pointer;
|
||||
|
||||
a {
|
||||
color: $grey-00;
|
||||
}
|
||||
}
|
||||
|
||||
&.profile > div {
|
||||
padding: 6px 12px;
|
||||
}
|
||||
|
||||
&.language {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: $unit;
|
||||
padding-right: $unit;
|
||||
|
||||
span {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.Switch {
|
||||
$height: 24px;
|
||||
|
||||
background: $grey-60;
|
||||
border-radius: calc($height / 2);
|
||||
border: none;
|
||||
position: relative;
|
||||
width: 44px;
|
||||
height: $height;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.Thumb {
|
||||
$diameter: 18px;
|
||||
|
||||
background: white;
|
||||
border-radius: calc($diameter / 2);
|
||||
display: block;
|
||||
height: $diameter;
|
||||
width: $diameter;
|
||||
transition: transform 100ms;
|
||||
transform: translateX(-2px);
|
||||
z-index: 3;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&[data-state="checked"] {
|
||||
background: white;
|
||||
transform: translateX(17px);
|
||||
}
|
||||
}
|
||||
|
||||
.left, .right {
|
||||
color: white;
|
||||
font-size: 10px;
|
||||
font-weight: $bold;
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.left {
|
||||
top: 6px;
|
||||
left: 6px;
|
||||
}
|
||||
|
||||
.right {
|
||||
top: 6px;
|
||||
right: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
&:hover:not(.disabled) {
|
||||
background: var(--menu-bg-item-hover);
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
|
||||
a {
|
||||
color: $grey-40;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
|
||||
&.profile > div {
|
||||
padding: 6px 12px;
|
||||
}
|
||||
|
||||
&.language {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: $unit;
|
||||
padding-right: $unit;
|
||||
|
||||
span {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
& > a, & > span {
|
||||
.Switch {
|
||||
$height: 24px;
|
||||
|
||||
background: $grey-60;
|
||||
border-radius: calc($height / 2);
|
||||
border: none;
|
||||
position: relative;
|
||||
width: 44px;
|
||||
height: $height;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.Thumb {
|
||||
$diameter: 18px;
|
||||
|
||||
background: $grey-100;
|
||||
border-radius: calc($diameter / 2);
|
||||
display: block;
|
||||
padding: 12px 12px;
|
||||
}
|
||||
|
||||
& > div {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 10px 12px;
|
||||
height: $diameter;
|
||||
width: $diameter;
|
||||
transition: transform 100ms;
|
||||
transform: translateX(-2px);
|
||||
z-index: 3;
|
||||
|
||||
&:hover {
|
||||
i.tag {
|
||||
background: $grey-60;
|
||||
color: white;
|
||||
}
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
span {
|
||||
flex-grow: 1;
|
||||
&[data-state='checked'] {
|
||||
background: $grey-100;
|
||||
transform: translateX(17px);
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
$diameter: 32px;
|
||||
border-radius: calc($diameter / 2);
|
||||
height: $diameter;
|
||||
width: $diameter;
|
||||
}
|
||||
.left,
|
||||
.right {
|
||||
color: $grey-100;
|
||||
font-size: 10px;
|
||||
font-weight: $bold;
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.left {
|
||||
top: 6px;
|
||||
left: 6px;
|
||||
}
|
||||
|
||||
.right {
|
||||
top: 6px;
|
||||
right: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: $grey-50;
|
||||
}
|
||||
|
||||
& > a,
|
||||
& > span {
|
||||
display: block;
|
||||
padding: 12px 12px;
|
||||
}
|
||||
|
||||
& > div {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 10px 12px;
|
||||
|
||||
&:hover {
|
||||
i.tag {
|
||||
background: var(--tag-bg);
|
||||
color: var(--tag-text);
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
img {
|
||||
$diameter: 32px;
|
||||
border-radius: calc($diameter / 2);
|
||||
height: $diameter;
|
||||
width: $diameter;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.MenuGroup {
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
border-bottom: 1px solid var(--menu-separator);
|
||||
|
||||
&:first-child .MenuItem:first-child:hover {
|
||||
border-top-left-radius: 6px;
|
||||
border-top-right-radius: 6px;
|
||||
}
|
||||
&:first-child .MenuItem:first-child:hover {
|
||||
border-top-left-radius: 6px;
|
||||
border-top-right-radius: 6px;
|
||||
}
|
||||
|
||||
&:last-child .MenuItem:last-child:hover {
|
||||
border-bottom-left-radius: 6px;
|
||||
border-bottom-right-radius: 6px;
|
||||
}
|
||||
&:last-child .MenuItem:last-child:hover {
|
||||
border-bottom-left-radius: 6px;
|
||||
border-bottom-right-radius: 6px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
import React, { useEffect, useState } from "react"
|
||||
import { getCookie, setCookie } from "cookies-next"
|
||||
import { useRouter } from "next/router"
|
||||
import { useTranslation } from "next-i18next"
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { getCookie, setCookie } from 'cookies-next'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
|
||||
import Link from "next/link"
|
||||
import * as Switch from "@radix-ui/react-switch"
|
||||
import Link from 'next/link'
|
||||
import * as Switch from '@radix-ui/react-switch'
|
||||
|
||||
import AboutModal from "~components/AboutModal"
|
||||
import AccountModal from "~components/AccountModal"
|
||||
import LoginModal from "~components/LoginModal"
|
||||
import SignupModal from "~components/SignupModal"
|
||||
import AboutModal from '~components/AboutModal'
|
||||
import AccountModal from '~components/AccountModal'
|
||||
import LoginModal from '~components/LoginModal'
|
||||
import SignupModal from '~components/SignupModal'
|
||||
|
||||
import "./index.scss"
|
||||
import './index.scss'
|
||||
|
||||
interface Props {
|
||||
authenticated: boolean
|
||||
|
|
@ -21,30 +21,30 @@ interface Props {
|
|||
|
||||
const HeaderMenu = (props: Props) => {
|
||||
const router = useRouter()
|
||||
const { t } = useTranslation("common")
|
||||
const { t } = useTranslation('common')
|
||||
|
||||
const accountCookie = getCookie("account")
|
||||
const accountCookie = getCookie('account')
|
||||
const accountData: AccountCookie = accountCookie
|
||||
? JSON.parse(accountCookie as string)
|
||||
: null
|
||||
|
||||
const userCookie = getCookie("user")
|
||||
const userCookie = getCookie('user')
|
||||
const userData: UserCookie = userCookie
|
||||
? JSON.parse(userCookie as string)
|
||||
: null
|
||||
|
||||
const localeCookie = getCookie("NEXT_LOCALE")
|
||||
const localeCookie = getCookie('NEXT_LOCALE')
|
||||
|
||||
const [checked, setChecked] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const locale = localeCookie
|
||||
setChecked(locale === "ja" ? true : false)
|
||||
setChecked(locale === 'ja' ? true : false)
|
||||
}, [localeCookie])
|
||||
|
||||
function handleCheckedChange(value: boolean) {
|
||||
const language = value ? "ja" : "en"
|
||||
setCookie("NEXT_LOCALE", language, { path: "/" })
|
||||
const language = value ? 'ja' : 'en'
|
||||
setCookie('NEXT_LOCALE', language, { path: '/' })
|
||||
router.push(router.asPath, undefined, { locale: language })
|
||||
}
|
||||
|
||||
|
|
@ -54,7 +54,7 @@ const HeaderMenu = (props: Props) => {
|
|||
<ul className="Menu auth">
|
||||
<div className="MenuGroup">
|
||||
<li className="MenuItem profile">
|
||||
<Link href={`/${accountData.username}` || ""} passHref>
|
||||
<Link href={`/${accountData.username}` || ''} passHref>
|
||||
<div>
|
||||
<span>{accountData.username}</span>
|
||||
<img
|
||||
|
|
@ -68,18 +68,18 @@ const HeaderMenu = (props: Props) => {
|
|||
</Link>
|
||||
</li>
|
||||
<li className="MenuItem">
|
||||
<Link href={`/saved` || ""}>{t("menu.saved")}</Link>
|
||||
<Link href={`/saved` || ''}>{t('menu.saved')}</Link>
|
||||
</li>
|
||||
</div>
|
||||
<div className="MenuGroup">
|
||||
<li className="MenuItem">
|
||||
<Link href="/teams">{t("menu.teams")}</Link>
|
||||
<Link href="/teams">{t('menu.teams')}</Link>
|
||||
</li>
|
||||
|
||||
<li className="MenuItem disabled">
|
||||
<div>
|
||||
<span>{t("menu.guides")}</span>
|
||||
<i className="tag">{t("coming_soon")}</i>
|
||||
<span>{t('menu.guides')}</span>
|
||||
<i className="tag">{t('coming_soon')}</i>
|
||||
</div>
|
||||
</li>
|
||||
</div>
|
||||
|
|
@ -87,7 +87,7 @@ const HeaderMenu = (props: Props) => {
|
|||
<AboutModal />
|
||||
<AccountModal />
|
||||
<li className="MenuItem" onClick={props.logout}>
|
||||
<span>{t("menu.logout")}</span>
|
||||
<span>{t('menu.logout')}</span>
|
||||
</li>
|
||||
</div>
|
||||
</ul>
|
||||
|
|
@ -100,7 +100,7 @@ const HeaderMenu = (props: Props) => {
|
|||
<ul className="Menu unauth">
|
||||
<div className="MenuGroup">
|
||||
<li className="MenuItem language">
|
||||
<span>{t("menu.language")}</span>
|
||||
<span>{t('menu.language')}</span>
|
||||
<Switch.Root
|
||||
className="Switch"
|
||||
onCheckedChange={handleCheckedChange}
|
||||
|
|
@ -114,13 +114,13 @@ const HeaderMenu = (props: Props) => {
|
|||
</div>
|
||||
<div className="MenuGroup">
|
||||
<li className="MenuItem">
|
||||
<Link href="/teams">{t("menu.teams")}</Link>
|
||||
<Link href="/teams">{t('menu.teams')}</Link>
|
||||
</li>
|
||||
|
||||
<li className="MenuItem disabled">
|
||||
<div>
|
||||
<span>{t("menu.guides")}</span>
|
||||
<i className="tag">{t("coming_soon")}</i>
|
||||
<span>{t('menu.guides')}</span>
|
||||
<i className="tag">{t('coming_soon')}</i>
|
||||
</div>
|
||||
</li>
|
||||
</div>
|
||||
|
|
|
|||
23
components/Input/index.scss
Normal file
23
components/Input/index.scss
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
.Input {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
background-color: var(--input-bg);
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
padding: $unit-2x;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.InputError {
|
||||
color: $error;
|
||||
font-size: $font-tiny;
|
||||
margin: $unit 0;
|
||||
padding: calc($unit / 2) ($unit * 2);
|
||||
}
|
||||
|
||||
::placeholder {
|
||||
/* Chrome, Firefox, Opera, Safari 10.1+ */
|
||||
color: var(--text-secondary) !important;
|
||||
opacity: 1; /* Firefox */
|
||||
}
|
||||
38
components/Input/index.tsx
Normal file
38
components/Input/index.tsx
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import classNames from 'classnames'
|
||||
import React from 'react'
|
||||
import './index.scss'
|
||||
|
||||
interface Props
|
||||
extends React.DetailedHTMLProps<
|
||||
React.InputHTMLAttributes<HTMLInputElement>,
|
||||
HTMLInputElement
|
||||
> {
|
||||
error?: string
|
||||
label?: string
|
||||
}
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, Props>(function input(
|
||||
props: Props,
|
||||
forwardedRef
|
||||
) {
|
||||
const classes = classNames({ Input: true }, props.className)
|
||||
|
||||
return (
|
||||
<label className="Label" htmlFor={props.name}>
|
||||
<input
|
||||
{...props}
|
||||
autoComplete="off"
|
||||
className={classes}
|
||||
defaultValue={props.value || ''}
|
||||
ref={forwardedRef}
|
||||
formNoValidate
|
||||
/>
|
||||
{props.label}
|
||||
{props.error && props.error.length > 0 && (
|
||||
<p className="InputError">{props.error}</p>
|
||||
)}
|
||||
</label>
|
||||
)
|
||||
})
|
||||
|
||||
export default Input
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
.Job.SelectTrigger {
|
||||
margin-bottom: $unit;
|
||||
}
|
||||
|
|
@ -1,11 +1,15 @@
|
|||
import React, { useEffect, useState } from "react"
|
||||
import { useRouter } from "next/router"
|
||||
import { useSnapshot } from "valtio"
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useSnapshot } from 'valtio'
|
||||
|
||||
import { appState } from "~utils/appState"
|
||||
import { jobGroups } from "~utils/jobGroups"
|
||||
import Select from '~components/Select'
|
||||
import SelectItem from '~components/SelectItem'
|
||||
import SelectGroup from '~components/SelectGroup'
|
||||
|
||||
import "./index.scss"
|
||||
import { appState } from '~utils/appState'
|
||||
import { jobGroups } from '~utils/jobGroups'
|
||||
|
||||
import './index.scss'
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
|
|
@ -20,12 +24,13 @@ const JobDropdown = React.forwardRef<HTMLSelectElement, Props>(
|
|||
function useFieldSet(props, ref) {
|
||||
// Set up router for locale
|
||||
const router = useRouter()
|
||||
const locale = router.locale || "en"
|
||||
const locale = router.locale || 'en'
|
||||
|
||||
// Create snapshot of app state
|
||||
const { party } = useSnapshot(appState)
|
||||
|
||||
// Set up local states for storing jobs
|
||||
const [open, setOpen] = useState(false)
|
||||
const [currentJob, setCurrentJob] = useState<Job>()
|
||||
const [jobs, setJobs] = useState<Job[]>()
|
||||
const [sortedJobs, setSortedJobs] = useState<GroupedJob>()
|
||||
|
|
@ -58,10 +63,14 @@ const JobDropdown = React.forwardRef<HTMLSelectElement, Props>(
|
|||
}
|
||||
}, [appState, props.currentJob])
|
||||
|
||||
function openJobSelect() {
|
||||
setOpen(!open)
|
||||
}
|
||||
|
||||
// Enable changing select value
|
||||
function handleChange(event: React.ChangeEvent<HTMLSelectElement>) {
|
||||
function handleChange(value: string) {
|
||||
if (jobs) {
|
||||
const job = jobs.find((job) => job.id === event.target.value)
|
||||
const job = jobs.find((job) => job.id === value)
|
||||
if (props.onChange) props.onChange(job)
|
||||
setCurrentJob(job)
|
||||
}
|
||||
|
|
@ -76,36 +85,37 @@ const JobDropdown = React.forwardRef<HTMLSelectElement, Props>(
|
|||
.sort((a, b) => a.order - b.order)
|
||||
.map((item, i) => {
|
||||
return (
|
||||
<option key={i} value={item.id}>
|
||||
<SelectItem key={i} value={item.id}>
|
||||
{item.name[locale]}
|
||||
</option>
|
||||
</SelectItem>
|
||||
)
|
||||
})
|
||||
|
||||
const groupName = jobGroups.find((g) => g.slug === group)?.name[locale]
|
||||
|
||||
return (
|
||||
<optgroup key={group} label={groupName}>
|
||||
<SelectGroup key={group} label={groupName} separator={false}>
|
||||
{options}
|
||||
</optgroup>
|
||||
</SelectGroup>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<select
|
||||
key={currentJob ? currentJob.id : -1}
|
||||
value={currentJob ? currentJob.id : -1}
|
||||
onBlur={props.onBlur}
|
||||
<Select
|
||||
trigger={'Select a class...'}
|
||||
placeholder={'Select a class...'}
|
||||
open={open}
|
||||
onClick={openJobSelect}
|
||||
onChange={handleChange}
|
||||
ref={ref}
|
||||
triggerClass="Job"
|
||||
>
|
||||
<option key="no-job" value={-1}>
|
||||
<SelectItem key={-1} value="no-job">
|
||||
No class
|
||||
</option>
|
||||
</SelectItem>
|
||||
{sortedJobs
|
||||
? Object.keys(sortedJobs).map((x) => renderJobGroup(x))
|
||||
: ""}
|
||||
</select>
|
||||
: ''}
|
||||
</Select>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -28,10 +28,10 @@
|
|||
}
|
||||
|
||||
.JobImage {
|
||||
$height: 249px;
|
||||
$height: 252px;
|
||||
$width: 447px;
|
||||
|
||||
background: url("/images/background_a.jpg");
|
||||
background: url('/images/background_a.jpg');
|
||||
background-size: 500px 281px;
|
||||
border-radius: $unit;
|
||||
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.2);
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
import React, { ForwardedRef, useEffect, useState } from "react"
|
||||
import { useRouter } from "next/router"
|
||||
import { useSnapshot } from "valtio"
|
||||
import { useTranslation } from "next-i18next"
|
||||
import React, { ForwardedRef, useEffect, useState } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
|
||||
import JobDropdown from "~components/JobDropdown"
|
||||
import JobSkillItem from "~components/JobSkillItem"
|
||||
import SearchModal from "~components/SearchModal"
|
||||
import JobDropdown from '~components/JobDropdown'
|
||||
import JobSkillItem from '~components/JobSkillItem'
|
||||
import SearchModal from '~components/SearchModal'
|
||||
|
||||
import { appState } from "~utils/appState"
|
||||
import { appState } from '~utils/appState'
|
||||
|
||||
import type { JobSkillObject, SearchableObject } from "~types"
|
||||
import type { JobSkillObject, SearchableObject } from '~types'
|
||||
|
||||
import "./index.scss"
|
||||
import './index.scss'
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
|
|
@ -24,14 +24,14 @@ interface Props {
|
|||
|
||||
const JobSection = (props: Props) => {
|
||||
const { party } = useSnapshot(appState)
|
||||
const { t } = useTranslation("common")
|
||||
const { t } = useTranslation('common')
|
||||
|
||||
const router = useRouter()
|
||||
const locale =
|
||||
router.locale && ["en", "ja"].includes(router.locale) ? router.locale : "en"
|
||||
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
|
||||
|
||||
const [job, setJob] = useState<Job>()
|
||||
const [imageUrl, setImageUrl] = useState("")
|
||||
const [imageUrl, setImageUrl] = useState('')
|
||||
const [numSkills, setNumSkills] = useState(4)
|
||||
const [skills, setSkills] = useState<{ [key: number]: JobSkill | undefined }>(
|
||||
[]
|
||||
|
|
@ -62,7 +62,7 @@ const JobSection = (props: Props) => {
|
|||
if (job) {
|
||||
if ((party.job && job.id != party.job.id) || !party.job)
|
||||
appState.party.job = job
|
||||
if (job.row === "1") setNumSkills(3)
|
||||
if (job.row === '1') setNumSkills(3)
|
||||
else setNumSkills(4)
|
||||
}
|
||||
}, [job])
|
||||
|
|
@ -75,11 +75,11 @@ const JobSection = (props: Props) => {
|
|||
}
|
||||
|
||||
function generateImageUrl() {
|
||||
let imgSrc = ""
|
||||
let imgSrc = ''
|
||||
|
||||
if (job) {
|
||||
const slug = job?.name.en.replaceAll(" ", "-").toLowerCase()
|
||||
const gender = party.user && party.user.gender == 1 ? "b" : "a"
|
||||
const slug = job?.name.en.replaceAll(' ', '-').toLowerCase()
|
||||
const gender = party.user && party.user.gender == 1 ? 'b' : 'a'
|
||||
|
||||
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/jobs/${slug}_${gender}.png`
|
||||
}
|
||||
|
|
@ -101,7 +101,7 @@ const JobSection = (props: Props) => {
|
|||
skill={skills[index]}
|
||||
editable={canEditSkill(skills[index])}
|
||||
key={`skill-${index}`}
|
||||
hasJob={job != undefined && job.id != "-1"}
|
||||
hasJob={job != undefined && job.id != '-1'}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -109,7 +109,7 @@ const JobSection = (props: Props) => {
|
|||
const editableSkillItem = (index: number) => {
|
||||
return (
|
||||
<SearchModal
|
||||
placeholderText={t("search.placeholders.job_skill")}
|
||||
placeholderText={t('search.placeholders.job_skill')}
|
||||
fromPosition={index}
|
||||
object="job_skills"
|
||||
job={job}
|
||||
|
|
|
|||
|
|
@ -15,13 +15,17 @@
|
|||
}
|
||||
|
||||
& p.placeholder {
|
||||
color: $grey-20;
|
||||
color: var(--text-tertiary-hover);
|
||||
}
|
||||
|
||||
& svg {
|
||||
fill: var(--icon-tertiary-hover);
|
||||
}
|
||||
}
|
||||
|
||||
& > img,
|
||||
& > div.placeholder {
|
||||
background: white;
|
||||
background: var(--card-bg);
|
||||
border-radius: calc($unit / 2);
|
||||
border: 1px solid rgba(0, 0, 0, 0);
|
||||
width: $unit * 5;
|
||||
|
|
@ -34,13 +38,17 @@
|
|||
justify-content: center;
|
||||
|
||||
& > svg {
|
||||
fill: $grey-60;
|
||||
fill: var(--icon-secondary);
|
||||
width: $unit * 2;
|
||||
height: $unit * 2;
|
||||
}
|
||||
}
|
||||
|
||||
p.placeholder {
|
||||
color: $grey-50;
|
||||
p {
|
||||
color: var(--text-primary);
|
||||
|
||||
&.placeholder {
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
import React from "react"
|
||||
import { useRouter } from "next/router"
|
||||
import { useTranslation } from "next-i18next"
|
||||
import React from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
|
||||
import classNames from "classnames"
|
||||
import PlusIcon from "~public/icons/Add.svg"
|
||||
import classNames from 'classnames'
|
||||
import PlusIcon from '~public/icons/Add.svg'
|
||||
|
||||
import "./index.scss"
|
||||
import './index.scss'
|
||||
|
||||
// Props
|
||||
interface Props extends React.ComponentPropsWithoutRef<"div"> {
|
||||
interface Props extends React.ComponentPropsWithoutRef<'div'> {
|
||||
skill?: JobSkill
|
||||
editable: boolean
|
||||
hasJob: boolean
|
||||
|
|
@ -17,11 +17,11 @@ interface Props extends React.ComponentPropsWithoutRef<"div"> {
|
|||
const JobSkillItem = React.forwardRef<HTMLDivElement, Props>(
|
||||
function useJobSkillItem({ ...props }, forwardedRef) {
|
||||
const router = useRouter()
|
||||
const { t } = useTranslation("common")
|
||||
const { t } = useTranslation('common')
|
||||
const locale =
|
||||
router.locale && ["en", "ja"].includes(router.locale)
|
||||
router.locale && ['en', 'ja'].includes(router.locale)
|
||||
? router.locale
|
||||
: "en"
|
||||
: 'en'
|
||||
|
||||
const classes = classNames({
|
||||
JobSkill: true,
|
||||
|
|
@ -47,7 +47,7 @@ const JobSkillItem = React.forwardRef<HTMLDivElement, Props>(
|
|||
} else {
|
||||
jsx = (
|
||||
<div className={imageClasses}>
|
||||
{props.editable && props.hasJob ? <PlusIcon /> : ""}
|
||||
{props.editable && props.hasJob ? <PlusIcon /> : ''}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -61,9 +61,9 @@ const JobSkillItem = React.forwardRef<HTMLDivElement, Props>(
|
|||
if (props.skill) {
|
||||
jsx = <p>{props.skill.name[locale]}</p>
|
||||
} else if (props.editable && props.hasJob) {
|
||||
jsx = <p className="placeholder">{t("job_skills.state.selectable")}</p>
|
||||
jsx = <p className="placeholder">{t('job_skills.state.selectable')}</p>
|
||||
} else {
|
||||
jsx = <p className="placeholder">{t("job_skills.state.no_skill")}</p>
|
||||
jsx = <p className="placeholder">{t('job_skills.state.no_skill')}</p>
|
||||
}
|
||||
|
||||
return jsx
|
||||
|
|
|
|||
|
|
@ -6,57 +6,57 @@
|
|||
align-items: center;
|
||||
|
||||
&:hover {
|
||||
background: $grey-90;
|
||||
background: var(--button-contained-bg);
|
||||
cursor: pointer;
|
||||
|
||||
.Info .skill.pill {
|
||||
background: $grey-80;
|
||||
.Info h5 {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.Info {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: calc($unit / 2);
|
||||
gap: $unit-half;
|
||||
width: 100%;
|
||||
|
||||
.skill.pill {
|
||||
background: $grey-90;
|
||||
border-radius: $unit * 2;
|
||||
color: $grey-00;
|
||||
color: $grey-15;
|
||||
display: inline;
|
||||
font-size: $font-tiny;
|
||||
font-weight: $medium;
|
||||
padding: calc($unit / 2) $unit;
|
||||
padding: $unit-half $unit;
|
||||
|
||||
&.buffing {
|
||||
background-color: $light-bg-dark;
|
||||
color: $light-text-dark;
|
||||
background-color: $light-bg-10;
|
||||
color: $light-text-10;
|
||||
}
|
||||
|
||||
&.debuffing {
|
||||
background-color: $water-bg-dark;
|
||||
color: $water-text-dark;
|
||||
background-color: $water-bg-10;
|
||||
color: $water-text-10;
|
||||
}
|
||||
|
||||
&.healing {
|
||||
background-color: $wind-bg-dark;
|
||||
color: $wind-text-dark;
|
||||
background-color: $wind-bg-10;
|
||||
color: $wind-text-10;
|
||||
}
|
||||
|
||||
&.damaging {
|
||||
background-color: $fire-bg-dark;
|
||||
color: $fire-text-dark;
|
||||
background-color: $fire-bg-10;
|
||||
color: $fire-text-10;
|
||||
}
|
||||
|
||||
&.field {
|
||||
background-color: $dark-bg-dark;
|
||||
color: $dark-text-dark;
|
||||
background-color: $dark-bg-20;
|
||||
color: $dark-text-10;
|
||||
}
|
||||
}
|
||||
|
||||
h5 {
|
||||
color: #555;
|
||||
color: var(--text-secondary);
|
||||
display: inline-block;
|
||||
font-size: $font-medium;
|
||||
font-weight: $medium;
|
||||
|
|
@ -65,7 +65,7 @@
|
|||
}
|
||||
|
||||
img {
|
||||
width: $unit * 6;
|
||||
height: $unit * 6;
|
||||
width: $unit-6x;
|
||||
height: $unit-6x;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import React, { useEffect, useState } from "react"
|
||||
import { useRouter } from "next/router"
|
||||
import { SkillGroup, skillClassification } from "~utils/skillGroups"
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { SkillGroup, skillClassification } from '~utils/skillGroups'
|
||||
|
||||
import "./index.scss"
|
||||
import './index.scss'
|
||||
|
||||
interface Props {
|
||||
data: JobSkill
|
||||
|
|
@ -12,7 +12,7 @@ interface Props {
|
|||
const JobSkillResult = (props: Props) => {
|
||||
const router = useRouter()
|
||||
const locale =
|
||||
router.locale && ["en", "ja"].includes(router.locale) ? router.locale : "en"
|
||||
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
|
||||
|
||||
const skill = props.data
|
||||
|
||||
|
|
@ -30,7 +30,7 @@ const JobSkillResult = (props: Props) => {
|
|||
<img alt={skill.name[locale]} src={jobSkillUrl()} />
|
||||
<div className="Info">
|
||||
<h5>{skill.name[locale]}</h5>
|
||||
<div className={`skill pill ${group?.name["en"].toLowerCase()}`}>
|
||||
<div className={`skill pill ${group?.name['en'].toLowerCase()}`}>
|
||||
{group?.name[locale]}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
.SearchFilterBar select {
|
||||
background-color: $grey-90;
|
||||
.SearchFilterBar .SelectTrigger {
|
||||
width: 100%;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import React, { useEffect, useState } from "react"
|
||||
import { useRouter } from "next/router"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { skillGroups } from "~utils/skillGroups"
|
||||
import Select from '~components/Select'
|
||||
import SelectItem from '~components/SelectItem'
|
||||
|
||||
import "./index.scss"
|
||||
import './index.scss'
|
||||
|
||||
interface Props {
|
||||
sendFilters: (filters: { [key: string]: number }) => void
|
||||
|
|
@ -12,15 +12,18 @@ interface Props {
|
|||
|
||||
const JobSkillSearchFilterBar = (props: Props) => {
|
||||
// Set up translation
|
||||
const { t } = useTranslation("common")
|
||||
const { t } = useTranslation('common')
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
const [currentGroup, setCurrentGroup] = useState(-1)
|
||||
|
||||
function onChange(event: React.ChangeEvent<HTMLSelectElement>) {
|
||||
setCurrentGroup(parseInt(event.target.value))
|
||||
function openSelect() {
|
||||
setOpen(!open)
|
||||
}
|
||||
|
||||
function onBlur(event: React.ChangeEvent<HTMLSelectElement>) {}
|
||||
function onChange(value: string) {
|
||||
setCurrentGroup(parseInt(value))
|
||||
}
|
||||
|
||||
function sendFilters() {
|
||||
const filters = {
|
||||
|
|
@ -36,34 +39,35 @@ const JobSkillSearchFilterBar = (props: Props) => {
|
|||
|
||||
return (
|
||||
<div className="SearchFilterBar">
|
||||
<select
|
||||
key="job-skill-groups"
|
||||
value={currentGroup}
|
||||
onBlur={onBlur}
|
||||
<Select
|
||||
defaultValue={-1}
|
||||
trigger={'All elements'}
|
||||
open={open}
|
||||
onChange={onChange}
|
||||
onClick={openSelect}
|
||||
>
|
||||
<option key="all" value={-1}>
|
||||
<SelectItem key="all" value={-1}>
|
||||
{t(`job_skills.all`)}
|
||||
</option>
|
||||
<option key="damaging" value={2}>
|
||||
</SelectItem>
|
||||
<SelectItem key="damaging" value={2}>
|
||||
{t(`job_skills.damaging`)}
|
||||
</option>
|
||||
<option key="buffing" value={0}>
|
||||
</SelectItem>
|
||||
<SelectItem key="buffing" value={0}>
|
||||
{t(`job_skills.buffing`)}
|
||||
</option>
|
||||
<option key="debuffing" value={1}>
|
||||
</SelectItem>
|
||||
<SelectItem key="debuffing" value={1}>
|
||||
{t(`job_skills.debuffing`)}
|
||||
</option>
|
||||
<option key="healing" value={3}>
|
||||
</SelectItem>
|
||||
<SelectItem key="healing" value={3}>
|
||||
{t(`job_skills.healing`)}
|
||||
</option>
|
||||
<option key="emp" value={4}>
|
||||
</SelectItem>
|
||||
<SelectItem key="emp" value={4}>
|
||||
{t(`job_skills.emp`)}
|
||||
</option>
|
||||
<option key="base" value={5}>
|
||||
</SelectItem>
|
||||
<SelectItem key="base" value={5}>
|
||||
{t(`job_skills.base`)}
|
||||
</option>
|
||||
</select>
|
||||
</SelectItem>
|
||||
</Select>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@ import type { ReactElement } from 'react'
|
|||
import TopHeader from '~components/TopHeader'
|
||||
|
||||
interface Props {
|
||||
children: ReactElement
|
||||
children: ReactElement
|
||||
}
|
||||
|
||||
const Layout = ({children}: Props) => {
|
||||
const Layout = ({ children }: Props) => {
|
||||
return (
|
||||
<>
|
||||
<TopHeader />
|
||||
|
|
@ -14,4 +14,4 @@ const Layout = ({children}: Props) => {
|
|||
)
|
||||
}
|
||||
|
||||
export default Layout
|
||||
export default Layout
|
||||
|
|
|
|||
|
|
@ -1,31 +1,31 @@
|
|||
.Login.Dialog form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: calc($unit / 2);
|
||||
margin-bottom: $unit;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: calc($unit / 2);
|
||||
margin-bottom: $unit;
|
||||
|
||||
.Button {
|
||||
font-size: $font-regular;
|
||||
padding: ($unit * 1.5) ($unit * 2);
|
||||
width: 100%;
|
||||
.Button {
|
||||
font-size: $font-regular;
|
||||
padding: ($unit * 1.5) ($unit * 2);
|
||||
width: 100%;
|
||||
|
||||
&.btn-disabled {
|
||||
background: $grey-90;
|
||||
color: $grey-70;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&:not(.btn-disabled) {
|
||||
background: $grey-90;
|
||||
color: $grey-40;
|
||||
|
||||
&:hover {
|
||||
background: $grey-80;
|
||||
}
|
||||
}
|
||||
&.btn-disabled {
|
||||
background: $grey-90;
|
||||
color: $grey-70;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
input {
|
||||
background: $grey-90;
|
||||
&:not(.btn-disabled) {
|
||||
background: $grey-90;
|
||||
color: $grey-50;
|
||||
|
||||
&:hover {
|
||||
background: $grey-80;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
background: $grey-90;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,19 @@
|
|||
import React, { useState } from "react"
|
||||
import { setCookie } from "cookies-next"
|
||||
import Router, { useRouter } from "next/router"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { AxiosResponse } from "axios"
|
||||
import React, { useState } from 'react'
|
||||
import { setCookie } from 'cookies-next'
|
||||
import Router, { useRouter } from 'next/router'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { AxiosResponse } from 'axios'
|
||||
|
||||
import * as Dialog from "@radix-ui/react-dialog"
|
||||
import * as Dialog from '@radix-ui/react-dialog'
|
||||
|
||||
import api from "~utils/api"
|
||||
import { accountState } from "~utils/accountState"
|
||||
import api from '~utils/api'
|
||||
import { accountState } from '~utils/accountState'
|
||||
|
||||
import Button from "~components/Button"
|
||||
import Fieldset from "~components/Fieldset"
|
||||
import Button from '~components/Button'
|
||||
import Fieldset from '~components/Input'
|
||||
|
||||
import CrossIcon from "~public/icons/Cross.svg"
|
||||
import "./index.scss"
|
||||
import CrossIcon from '~public/icons/Cross.svg'
|
||||
import './index.scss'
|
||||
|
||||
interface Props {}
|
||||
|
||||
|
|
@ -28,13 +28,13 @@ const emailRegex =
|
|||
|
||||
const LoginModal = (props: Props) => {
|
||||
const router = useRouter()
|
||||
const { t } = useTranslation("common")
|
||||
const { t } = useTranslation('common')
|
||||
|
||||
// Set up form states and error handling
|
||||
const [formValid, setFormValid] = useState(false)
|
||||
const [errors, setErrors] = useState<ErrorMap>({
|
||||
email: "",
|
||||
password: "",
|
||||
email: '',
|
||||
password: '',
|
||||
})
|
||||
|
||||
// States
|
||||
|
|
@ -50,17 +50,17 @@ const LoginModal = (props: Props) => {
|
|||
let newErrors = { ...errors }
|
||||
|
||||
switch (name) {
|
||||
case "email":
|
||||
case 'email':
|
||||
if (value.length == 0)
|
||||
newErrors.email = t("modals.login.errors.empty_email")
|
||||
newErrors.email = t('modals.login.errors.empty_email')
|
||||
else if (!emailRegex.test(value))
|
||||
newErrors.email = t("modals.login.errors.invalid_email")
|
||||
else newErrors.email = ""
|
||||
newErrors.email = t('modals.login.errors.invalid_email')
|
||||
else newErrors.email = ''
|
||||
break
|
||||
|
||||
case "password":
|
||||
case 'password':
|
||||
newErrors.password =
|
||||
value.length == 0 ? t("modals.login.errors.empty_password") : ""
|
||||
value.length == 0 ? t('modals.login.errors.empty_password') : ''
|
||||
break
|
||||
|
||||
default:
|
||||
|
|
@ -91,7 +91,7 @@ const LoginModal = (props: Props) => {
|
|||
const body = {
|
||||
email: emailInput.current?.value,
|
||||
password: passwordInput.current?.value,
|
||||
grant_type: "password",
|
||||
grant_type: 'password',
|
||||
}
|
||||
|
||||
if (formValid) {
|
||||
|
|
@ -119,7 +119,7 @@ const LoginModal = (props: Props) => {
|
|||
token: response.data.access_token,
|
||||
}
|
||||
|
||||
setCookie("account", cookieObj, { path: "/" })
|
||||
setCookie('account', cookieObj, { path: '/' })
|
||||
}
|
||||
|
||||
function storeUserInfo(response: AxiosResponse) {
|
||||
|
|
@ -132,7 +132,7 @@ const LoginModal = (props: Props) => {
|
|||
gender: user.gender,
|
||||
}
|
||||
|
||||
setCookie("user", cookieObj, { path: "/" })
|
||||
setCookie('user', cookieObj, { path: '/' })
|
||||
|
||||
accountState.account.user = {
|
||||
id: user.id,
|
||||
|
|
@ -142,7 +142,7 @@ const LoginModal = (props: Props) => {
|
|||
gender: user.gender,
|
||||
}
|
||||
|
||||
console.log("Authorizing account...")
|
||||
console.log('Authorizing account...')
|
||||
accountState.account.authorized = true
|
||||
|
||||
setOpen(false)
|
||||
|
|
@ -151,7 +151,7 @@ const LoginModal = (props: Props) => {
|
|||
|
||||
function changeLanguage(newLanguage: string) {
|
||||
if (newLanguage !== router.locale) {
|
||||
setCookie("NEXT_LOCALE", newLanguage, { path: "/" })
|
||||
setCookie('NEXT_LOCALE', newLanguage, { path: '/' })
|
||||
router.push(router.asPath, undefined, { locale: newLanguage })
|
||||
}
|
||||
}
|
||||
|
|
@ -159,8 +159,8 @@ const LoginModal = (props: Props) => {
|
|||
function openChange(open: boolean) {
|
||||
setOpen(open)
|
||||
setErrors({
|
||||
email: "",
|
||||
password: "",
|
||||
email: '',
|
||||
password: '',
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -168,7 +168,7 @@ const LoginModal = (props: Props) => {
|
|||
<Dialog.Root open={open} onOpenChange={openChange}>
|
||||
<Dialog.Trigger asChild>
|
||||
<li className="MenuItem">
|
||||
<span>{t("menu.login")}</span>
|
||||
<span>{t('menu.login')}</span>
|
||||
</li>
|
||||
</Dialog.Trigger>
|
||||
<Dialog.Portal>
|
||||
|
|
@ -178,7 +178,7 @@ const LoginModal = (props: Props) => {
|
|||
>
|
||||
<div className="DialogHeader">
|
||||
<Dialog.Title className="DialogTitle">
|
||||
{t("modals.login.title")}
|
||||
{t('modals.login.title')}
|
||||
</Dialog.Title>
|
||||
<Dialog.Close className="DialogClose" asChild>
|
||||
<span>
|
||||
|
|
@ -190,7 +190,7 @@ const LoginModal = (props: Props) => {
|
|||
<form className="form" onSubmit={login}>
|
||||
<Fieldset
|
||||
fieldName="email"
|
||||
placeholder={t("modals.login.placeholders.email")}
|
||||
placeholder={t('modals.login.placeholders.email')}
|
||||
onChange={handleChange}
|
||||
error={errors.email}
|
||||
ref={emailInput}
|
||||
|
|
@ -198,13 +198,13 @@ const LoginModal = (props: Props) => {
|
|||
|
||||
<Fieldset
|
||||
fieldName="password"
|
||||
placeholder={t("modals.login.placeholders.password")}
|
||||
placeholder={t('modals.login.placeholders.password')}
|
||||
onChange={handleChange}
|
||||
error={errors.password}
|
||||
ref={passwordInput}
|
||||
/>
|
||||
|
||||
<Button>{t("modals.login.buttons.confirm")}</Button>
|
||||
<Button>{t('modals.login.buttons.confirm')}</Button>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
<Dialog.Overlay className="Overlay" />
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
#Party .Extra {
|
||||
color: #888;
|
||||
display: flex;
|
||||
font-weight: 500;
|
||||
gap: 8px;
|
||||
line-height: 34px;
|
||||
}
|
||||
color: #888;
|
||||
display: flex;
|
||||
font-weight: 500;
|
||||
gap: 8px;
|
||||
line-height: 34px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,20 @@
|
|||
import React, { useCallback, useEffect, useMemo, useState } from "react"
|
||||
import { useRouter } from "next/router"
|
||||
import { useSnapshot } from "valtio"
|
||||
import { getCookie } from "cookies-next"
|
||||
import clonedeep from "lodash.clonedeep"
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import { getCookie } from 'cookies-next'
|
||||
import clonedeep from 'lodash.clonedeep'
|
||||
|
||||
import PartySegmentedControl from "~components/PartySegmentedControl"
|
||||
import PartyDetails from "~components/PartyDetails"
|
||||
import WeaponGrid from "~components/WeaponGrid"
|
||||
import SummonGrid from "~components/SummonGrid"
|
||||
import CharacterGrid from "~components/CharacterGrid"
|
||||
import PartySegmentedControl from '~components/PartySegmentedControl'
|
||||
import PartyDetails from '~components/PartyDetails'
|
||||
import WeaponGrid from '~components/WeaponGrid'
|
||||
import SummonGrid from '~components/SummonGrid'
|
||||
import CharacterGrid from '~components/CharacterGrid'
|
||||
|
||||
import api from "~utils/api"
|
||||
import { appState, initialAppState } from "~utils/appState"
|
||||
import { GridType, TeamElement } from "~utils/enums"
|
||||
import api from '~utils/api'
|
||||
import { appState, initialAppState } from '~utils/appState'
|
||||
import { GridType, TeamElement } from '~utils/enums'
|
||||
|
||||
import "./index.scss"
|
||||
import './index.scss'
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
|
|
@ -26,7 +26,7 @@ interface Props {
|
|||
|
||||
const Party = (props: Props) => {
|
||||
// Cookies
|
||||
const cookie = getCookie("account")
|
||||
const cookie = getCookie('account')
|
||||
const accountData: AccountCookie = cookie
|
||||
? JSON.parse(cookie as string)
|
||||
: null
|
||||
|
|
@ -113,7 +113,7 @@ const Party = (props: Props) => {
|
|||
.destroy({ id: appState.party.id, params: headers })
|
||||
.then(() => {
|
||||
// Push to route
|
||||
router.push("/")
|
||||
router.push('/')
|
||||
|
||||
// Clean state
|
||||
const resetState = clonedeep(initialAppState)
|
||||
|
|
@ -188,16 +188,16 @@ const Party = (props: Props) => {
|
|||
// Methods: Navigating with segmented control
|
||||
function segmentClicked(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
switch (event.target.value) {
|
||||
case "class":
|
||||
case 'class':
|
||||
setCurrentTab(GridType.Class)
|
||||
break
|
||||
case "characters":
|
||||
case 'characters':
|
||||
setCurrentTab(GridType.Character)
|
||||
break
|
||||
case "weapons":
|
||||
case 'weapons':
|
||||
setCurrentTab(GridType.Weapon)
|
||||
break
|
||||
case "summons":
|
||||
case 'summons':
|
||||
setCurrentTab(GridType.Summon)
|
||||
break
|
||||
default:
|
||||
|
|
@ -253,7 +253,7 @@ const Party = (props: Props) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<React.Fragment>
|
||||
{navigation}
|
||||
<section id="Party">{currentGrid()}</section>
|
||||
{
|
||||
|
|
@ -263,7 +263,7 @@ const Party = (props: Props) => {
|
|||
deleteCallback={deleteTeam}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,153 +1,159 @@
|
|||
.PartyDetails {
|
||||
display: none; // This breaks transition, find a workaround
|
||||
opacity: 0;
|
||||
margin: 0 auto;
|
||||
margin-bottom: 100px;
|
||||
max-width: $unit * 95;
|
||||
position: relative;
|
||||
display: none; // This breaks transition, find a workaround
|
||||
opacity: 0;
|
||||
margin: $unit-4x auto 0;
|
||||
max-width: $unit * 94;
|
||||
position: relative;
|
||||
|
||||
&.Editable {
|
||||
top: $unit;
|
||||
height: 0;
|
||||
z-index: 2;
|
||||
transition: opacity 0.2s ease-in-out,
|
||||
top 0.2s ease-in-out;
|
||||
&.Editable {
|
||||
top: $unit;
|
||||
height: 0;
|
||||
z-index: 2;
|
||||
transition: opacity 0.2s ease-in-out, top 0.2s ease-in-out;
|
||||
|
||||
|
||||
&.Visible {
|
||||
display: block;
|
||||
height: auto;
|
||||
margin-bottom: 40vh;
|
||||
opacity: 1;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
display: block;
|
||||
width: 100%;
|
||||
|
||||
textarea {
|
||||
min-height: $unit * 20;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.bottom {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: $unit;
|
||||
|
||||
.left {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.right {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: $unit;
|
||||
}
|
||||
}
|
||||
&.Visible {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit;
|
||||
height: auto;
|
||||
opacity: 1;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
&.ReadOnly {
|
||||
top: $unit * -1;
|
||||
transition: opacity 0.2s ease-in-out,
|
||||
top 0.2s ease-in-out;
|
||||
fieldset {
|
||||
display: block;
|
||||
width: 100%;
|
||||
|
||||
&.Visible {
|
||||
display: block;
|
||||
height: auto;
|
||||
opacity: 1;
|
||||
top: 0;
|
||||
}
|
||||
textarea {
|
||||
min-height: $unit * 20;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.bottom {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: $unit;
|
||||
margin-bottom: $unit-12x;
|
||||
|
||||
p {
|
||||
font-size: $font-regular;
|
||||
line-height: $font-regular * 1.2;
|
||||
white-space: pre-line;
|
||||
}
|
||||
.left {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.right {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: $unit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.ReadOnly {
|
||||
top: $unit * -1;
|
||||
transition: opacity 0.2s ease-in-out, top 0.2s ease-in-out;
|
||||
|
||||
&.Visible {
|
||||
display: block;
|
||||
height: auto;
|
||||
opacity: 1;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: $font-regular;
|
||||
line-height: $font-regular * 1.2;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: $font-xlarge;
|
||||
font-weight: $normal;
|
||||
text-align: left;
|
||||
margin-bottom: $unit;
|
||||
}
|
||||
|
||||
.info {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: $unit;
|
||||
margin-bottom: $unit * 2;
|
||||
|
||||
.left {
|
||||
flex-grow: 1;
|
||||
|
||||
h1 {
|
||||
font-size: $font-xlarge;
|
||||
font-weight: $normal;
|
||||
text-align: left;
|
||||
margin-bottom: $unit;
|
||||
}
|
||||
|
||||
.info {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: $unit;
|
||||
margin-bottom: $unit * 2;
|
||||
|
||||
.left {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.attribution {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
& > div {
|
||||
align-items: center;
|
||||
display: inline-flex;
|
||||
font-size: $font-small;
|
||||
height: 26px;
|
||||
}
|
||||
|
||||
time {
|
||||
font-size: $font-small;
|
||||
}
|
||||
|
||||
& > *:not(:last-child):after {
|
||||
content: " · ";
|
||||
margin: 0 calc($unit / 2);
|
||||
}
|
||||
}
|
||||
|
||||
.user {
|
||||
align-items: center;
|
||||
display: inline-flex;
|
||||
gap: calc($unit / 2);
|
||||
margin-top: 1px;
|
||||
|
||||
img, .no-user {
|
||||
$diameter: 24px;
|
||||
|
||||
border-radius: calc($diameter / 2);
|
||||
height: $diameter;
|
||||
width: $diameter;
|
||||
}
|
||||
|
||||
img.gran {
|
||||
background-color: #CEE7FE;
|
||||
}
|
||||
|
||||
img.djeeta {
|
||||
background-color: #FFE1FE;
|
||||
}
|
||||
|
||||
.no-user {
|
||||
background: $grey-80;
|
||||
}
|
||||
color: var(--text-primary);
|
||||
|
||||
&.empty {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.attribution {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
& > div {
|
||||
align-items: center;
|
||||
display: inline-flex;
|
||||
font-size: $font-small;
|
||||
height: 26px;
|
||||
}
|
||||
|
||||
time {
|
||||
font-size: $font-small;
|
||||
}
|
||||
|
||||
& > *:not(:last-child):after {
|
||||
content: ' · ';
|
||||
margin: 0 calc($unit / 2);
|
||||
}
|
||||
}
|
||||
|
||||
.user {
|
||||
align-items: center;
|
||||
display: inline-flex;
|
||||
gap: calc($unit / 2);
|
||||
margin-top: 1px;
|
||||
|
||||
img,
|
||||
.no-user {
|
||||
$diameter: 24px;
|
||||
|
||||
border-radius: calc($diameter / 2);
|
||||
height: $diameter;
|
||||
width: $diameter;
|
||||
}
|
||||
|
||||
img.gran {
|
||||
background-color: #cee7fe;
|
||||
}
|
||||
|
||||
img.djeeta {
|
||||
background-color: #ffe1fe;
|
||||
}
|
||||
|
||||
.no-user {
|
||||
background: $grey-80;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.EmptyDetails {
|
||||
display: none;
|
||||
justify-content: center;
|
||||
margin-bottom: $unit * 10;
|
||||
display: none;
|
||||
justify-content: center;
|
||||
margin: $unit-4x 0 $unit-10x;
|
||||
|
||||
&.Visible {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
&.Visible {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,34 +1,37 @@
|
|||
import React, { useState } from "react"
|
||||
import Head from "next/head"
|
||||
import { useRouter } from "next/router"
|
||||
import { useSnapshot } from "valtio"
|
||||
import { useTranslation } from "next-i18next"
|
||||
import React, { useState } from 'react'
|
||||
import Head from 'next/head'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
|
||||
import Linkify from "react-linkify"
|
||||
import classNames from "classnames"
|
||||
import Linkify from 'react-linkify'
|
||||
import classNames from 'classnames'
|
||||
|
||||
import * as AlertDialog from "@radix-ui/react-alert-dialog"
|
||||
import CrossIcon from "~public/icons/Cross.svg"
|
||||
import * as AlertDialog from '@radix-ui/react-alert-dialog'
|
||||
|
||||
import Button from "~components/Button"
|
||||
import CharLimitedFieldset from "~components/CharLimitedFieldset"
|
||||
import RaidDropdown from "~components/RaidDropdown"
|
||||
import TextFieldset from "~components/TextFieldset"
|
||||
import Button from '~components/Button'
|
||||
import CharLimitedFieldset from '~components/CharLimitedFieldset'
|
||||
import RaidDropdown from '~components/RaidDropdown'
|
||||
import TextFieldset from '~components/TextFieldset'
|
||||
|
||||
import { accountState } from "~utils/accountState"
|
||||
import { appState } from "~utils/appState"
|
||||
import { accountState } from '~utils/accountState'
|
||||
import { appState } from '~utils/appState'
|
||||
|
||||
import "./index.scss"
|
||||
import Link from "next/link"
|
||||
import { formatTimeAgo } from "~utils/timeAgo"
|
||||
import CheckIcon from '~public/icons/Check.svg'
|
||||
import CrossIcon from '~public/icons/Cross.svg'
|
||||
import EditIcon from '~public/icons/Edit.svg'
|
||||
|
||||
import './index.scss'
|
||||
import Link from 'next/link'
|
||||
import { formatTimeAgo } from '~utils/timeAgo'
|
||||
|
||||
const emptyRaid: Raid = {
|
||||
id: "",
|
||||
id: '',
|
||||
name: {
|
||||
en: "",
|
||||
ja: "",
|
||||
en: '',
|
||||
ja: '',
|
||||
},
|
||||
slug: "",
|
||||
slug: '',
|
||||
level: 0,
|
||||
group: 0,
|
||||
element: 0,
|
||||
|
|
@ -47,9 +50,9 @@ const PartyDetails = (props: Props) => {
|
|||
const { party, raids } = useSnapshot(appState)
|
||||
const { account } = useSnapshot(accountState)
|
||||
|
||||
const { t } = useTranslation("common")
|
||||
const { t } = useTranslation('common')
|
||||
const router = useRouter()
|
||||
const locale = router.locale || "en"
|
||||
const locale = router.locale || 'en'
|
||||
|
||||
const nameInput = React.createRef<HTMLInputElement>()
|
||||
const descriptionInput = React.createRef<HTMLTextAreaElement>()
|
||||
|
|
@ -87,8 +90,8 @@ const PartyDetails = (props: Props) => {
|
|||
})
|
||||
|
||||
const [errors, setErrors] = useState<{ [key: string]: string }>({
|
||||
name: "",
|
||||
description: "",
|
||||
name: '',
|
||||
description: '',
|
||||
})
|
||||
|
||||
function handleInputChange(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
|
|
@ -140,7 +143,7 @@ const PartyDetails = (props: Props) => {
|
|||
return (
|
||||
<div className={userClass}>
|
||||
{userImage()}
|
||||
{party.user ? party.user.username : t("no_user")}
|
||||
{party.user ? party.user.username : t('no_user')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -169,30 +172,30 @@ const PartyDetails = (props: Props) => {
|
|||
if (party.editable) {
|
||||
return (
|
||||
<AlertDialog.Root>
|
||||
<AlertDialog.Trigger className="Button destructive">
|
||||
<span className="icon">
|
||||
<AlertDialog.Trigger className="Button Blended medium destructive">
|
||||
<span className="Accessory">
|
||||
<CrossIcon />
|
||||
</span>
|
||||
<span className="text">{t("buttons.delete")}</span>
|
||||
<span className="Text">{t('buttons.delete')}</span>
|
||||
</AlertDialog.Trigger>
|
||||
<AlertDialog.Portal>
|
||||
<AlertDialog.Overlay className="Overlay" />
|
||||
<AlertDialog.Content className="Dialog">
|
||||
<AlertDialog.Title className="DialogTitle">
|
||||
{t("modals.delete_team.title")}
|
||||
{t('modals.delete_team.title')}
|
||||
</AlertDialog.Title>
|
||||
<AlertDialog.Description className="DialogDescription">
|
||||
{t("modals.delete_team.description")}
|
||||
{t('modals.delete_team.description')}
|
||||
</AlertDialog.Description>
|
||||
<div className="actions">
|
||||
<AlertDialog.Cancel className="Button modal">
|
||||
{t("modals.delete_team.buttons.cancel")}
|
||||
{t('modals.delete_team.buttons.cancel')}
|
||||
</AlertDialog.Cancel>
|
||||
<AlertDialog.Action
|
||||
className="Button modal destructive"
|
||||
onClick={(e) => props.deleteCallback(e)}
|
||||
>
|
||||
{t("modals.delete_team.buttons.confirm")}
|
||||
{t('modals.delete_team.buttons.confirm')}
|
||||
</AlertDialog.Action>
|
||||
</div>
|
||||
</AlertDialog.Content>
|
||||
|
|
@ -200,7 +203,7 @@ const PartyDetails = (props: Props) => {
|
|||
</AlertDialog.Root>
|
||||
)
|
||||
} else {
|
||||
return ""
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -217,13 +220,13 @@ const PartyDetails = (props: Props) => {
|
|||
/>
|
||||
<RaidDropdown
|
||||
showAllRaidsOption={false}
|
||||
currentRaid={party.raid?.slug || ""}
|
||||
currentRaid={party.raid?.slug || ''}
|
||||
ref={raidSelect}
|
||||
/>
|
||||
<TextFieldset
|
||||
fieldName="name"
|
||||
placeholder={
|
||||
"Write your notes here\n\n\nWatch out for the 50% trigger!\nMake sure to click Fediel’s 1 first\nGood luck with RNG!"
|
||||
'Write your notes here\n\n\nWatch out for the 50% trigger!\nMake sure to click Fediel’s 1 first\nGood luck with RNG!'
|
||||
}
|
||||
value={party.description}
|
||||
onChange={handleTextAreaChange}
|
||||
|
|
@ -233,16 +236,15 @@ const PartyDetails = (props: Props) => {
|
|||
|
||||
<div className="bottom">
|
||||
<div className="left">
|
||||
{router.pathname !== "/new" ? deleteButton() : ""}
|
||||
{router.pathname !== '/new' ? deleteButton() : ''}
|
||||
</div>
|
||||
<div className="right">
|
||||
<Button active={true} onClick={toggleDetails}>
|
||||
{t("buttons.cancel")}
|
||||
</Button>
|
||||
|
||||
<Button active={true} icon="check" onClick={updateDetails}>
|
||||
{t("buttons.save_info")}
|
||||
</Button>
|
||||
<Button text={t('buttons.cancel')} onClick={toggleDetails} />
|
||||
<Button
|
||||
accessoryIcon={<CheckIcon className="Check" />}
|
||||
text={t('buttons.save_info')}
|
||||
onClick={updateDetails}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
@ -252,10 +254,12 @@ const PartyDetails = (props: Props) => {
|
|||
<section className={readOnlyClasses}>
|
||||
<div className="info">
|
||||
<div className="left">
|
||||
{party.name ? <h1>{party.name}</h1> : ""}
|
||||
<h1 className={!party.name ? 'empty' : ''}>
|
||||
{party.name ? party.name : 'Untitled'}
|
||||
</h1>
|
||||
<div className="attribution">
|
||||
{party.user ? linkedUserBlock(party.user) : userBlock()}
|
||||
{party.raid ? linkedRaidBlock(party.raid) : ""}
|
||||
{party.raid ? linkedRaidBlock(party.raid) : ''}
|
||||
{party.created_at != undefined ? (
|
||||
<time
|
||||
className="last-updated"
|
||||
|
|
@ -264,15 +268,17 @@ const PartyDetails = (props: Props) => {
|
|||
{formatTimeAgo(new Date(party.created_at), locale)}
|
||||
</time>
|
||||
) : (
|
||||
""
|
||||
''
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="right">
|
||||
{party.editable ? (
|
||||
<Button active={true} icon="edit" onClick={toggleDetails}>
|
||||
{t("buttons.show_info")}
|
||||
</Button>
|
||||
<Button
|
||||
accessoryIcon={<EditIcon />}
|
||||
text={t('buttons.show_info')}
|
||||
onClick={toggleDetails}
|
||||
/>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
|
|
@ -283,7 +289,7 @@ const PartyDetails = (props: Props) => {
|
|||
<Linkify>{party.description}</Linkify>
|
||||
</p>
|
||||
) : (
|
||||
""
|
||||
''
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
|
|
@ -291,59 +297,24 @@ const PartyDetails = (props: Props) => {
|
|||
const emptyDetails = (
|
||||
<div className={emptyClasses}>
|
||||
{party.editable ? (
|
||||
<Button active={true} icon="edit" onClick={toggleDetails}>
|
||||
{t("buttons.show_info")}
|
||||
</Button>
|
||||
<Button
|
||||
accessoryIcon={<EditIcon />}
|
||||
text={t('buttons.show_info')}
|
||||
onClick={toggleDetails}
|
||||
/>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
const generateTitle = () => {
|
||||
let title = party.raid ? `[${party.raid?.name[locale]}] ` : ""
|
||||
|
||||
const username =
|
||||
party.user != null ? `@${party.user?.username}` : t("header.anonymous")
|
||||
|
||||
if (party.name != null)
|
||||
title += t("header.byline", { partyName: party.name, username: username })
|
||||
else if (party.name == null && party.editable && router.route === "/new")
|
||||
title = t("header.new_team")
|
||||
else
|
||||
title += t("header.untitled_team", {
|
||||
username: username,
|
||||
})
|
||||
|
||||
return title
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Head>
|
||||
<title>{generateTitle()}</title>
|
||||
|
||||
<meta property="og:title" content={generateTitle()} />
|
||||
<meta
|
||||
property="og:description"
|
||||
content={party.description ? party.description : ""}
|
||||
/>
|
||||
<meta property="og:url" content="https://app.granblue.team" />
|
||||
<meta property="og:type" content="website" />
|
||||
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta property="twitter:domain" content="app.granblue.team" />
|
||||
<meta name="twitter:title" content={generateTitle()} />
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content={party.description ? party.description : ""}
|
||||
/>
|
||||
</Head>
|
||||
<React.Fragment>
|
||||
{editable && (party.name || party.description || party.raid)
|
||||
? readOnly
|
||||
: emptyDetails}
|
||||
{editable}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,20 +1,20 @@
|
|||
.PartyNavigation {
|
||||
display: flex;
|
||||
gap: 58px;
|
||||
justify-content: center;
|
||||
margin: 0 auto;
|
||||
margin-bottom: $unit * 3;
|
||||
max-width: 760px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
gap: 58px;
|
||||
justify-content: center;
|
||||
margin: 0 auto;
|
||||
margin-bottom: $unit * 3;
|
||||
max-width: 760px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ExtraSwitch {
|
||||
color: #888;
|
||||
display: flex;
|
||||
font-weight: $normal;
|
||||
gap: 8px;
|
||||
line-height: 34px;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
}
|
||||
color: #888;
|
||||
display: flex;
|
||||
font-weight: $normal;
|
||||
gap: 8px;
|
||||
line-height: 34px;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,89 +10,104 @@ import ToggleSwitch from '~components/ToggleSwitch'
|
|||
|
||||
import { GridType } from '~utils/enums'
|
||||
|
||||
|
||||
import './index.scss'
|
||||
|
||||
interface Props {
|
||||
selectedTab: GridType
|
||||
onClick: (event: React.ChangeEvent<HTMLInputElement>) => void
|
||||
onCheckboxChange: (event: React.ChangeEvent<HTMLInputElement>) => void
|
||||
selectedTab: GridType
|
||||
onClick: (event: React.ChangeEvent<HTMLInputElement>) => void
|
||||
onCheckboxChange: (event: React.ChangeEvent<HTMLInputElement>) => void
|
||||
}
|
||||
|
||||
const PartySegmentedControl = (props: Props) => {
|
||||
const { t } = useTranslation('common')
|
||||
const { t } = useTranslation('common')
|
||||
|
||||
const { party, grid } = useSnapshot(appState)
|
||||
const { party, grid } = useSnapshot(appState)
|
||||
|
||||
function getElement() {
|
||||
let element: number = 0
|
||||
if (party.element == 0 && grid.weapons.mainWeapon)
|
||||
element = grid.weapons.mainWeapon.element
|
||||
else
|
||||
element = party.element
|
||||
function getElement() {
|
||||
let element: number = 0
|
||||
if (party.element == 0 && grid.weapons.mainWeapon)
|
||||
element = grid.weapons.mainWeapon.element
|
||||
else element = party.element
|
||||
|
||||
switch(element) {
|
||||
case 1: return "wind"; break
|
||||
case 2: return "fire"; break
|
||||
case 3: return "water"; break
|
||||
case 4: return "earth"; break
|
||||
case 5: return "dark"; break
|
||||
case 6: return "light"; break
|
||||
}
|
||||
switch (element) {
|
||||
case 1:
|
||||
return 'wind'
|
||||
break
|
||||
case 2:
|
||||
return 'fire'
|
||||
break
|
||||
case 3:
|
||||
return 'water'
|
||||
break
|
||||
case 4:
|
||||
return 'earth'
|
||||
break
|
||||
case 5:
|
||||
return 'dark'
|
||||
break
|
||||
case 6:
|
||||
return 'light'
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const extraToggle =
|
||||
<div className="ExtraSwitch">
|
||||
Extra
|
||||
<ToggleSwitch
|
||||
name="ExtraSwitch"
|
||||
editable={party.editable}
|
||||
checked={party.extra}
|
||||
onChange={props.onCheckboxChange}
|
||||
/>
|
||||
</div>
|
||||
const extraToggle = (
|
||||
<div className="ExtraSwitch">
|
||||
Extra
|
||||
<ToggleSwitch
|
||||
name="ExtraSwitch"
|
||||
editable={party.editable}
|
||||
checked={party.extra}
|
||||
onChange={props.onCheckboxChange}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="PartyNavigation">
|
||||
<SegmentedControl elementClass={getElement()}>
|
||||
{/* <Segment
|
||||
return (
|
||||
<div className="PartyNavigation">
|
||||
<SegmentedControl elementClass={getElement()}>
|
||||
{/* <Segment
|
||||
groupName="grid"
|
||||
name="class"
|
||||
selected={props.selectedTab === GridType.Class}
|
||||
onClick={props.onClick}
|
||||
>Class</Segment> */}
|
||||
|
||||
<Segment
|
||||
groupName="grid"
|
||||
name="characters"
|
||||
selected={props.selectedTab == GridType.Character}
|
||||
onClick={props.onClick}
|
||||
>{t('party.segmented_control.characters')}</Segment>
|
||||
<Segment
|
||||
groupName="grid"
|
||||
name="characters"
|
||||
selected={props.selectedTab == GridType.Character}
|
||||
onClick={props.onClick}
|
||||
>
|
||||
{t('party.segmented_control.characters')}
|
||||
</Segment>
|
||||
|
||||
<Segment
|
||||
groupName="grid"
|
||||
name="weapons"
|
||||
selected={props.selectedTab == GridType.Weapon}
|
||||
onClick={props.onClick}
|
||||
>{t('party.segmented_control.weapons')}</Segment>
|
||||
<Segment
|
||||
groupName="grid"
|
||||
name="weapons"
|
||||
selected={props.selectedTab == GridType.Weapon}
|
||||
onClick={props.onClick}
|
||||
>
|
||||
{t('party.segmented_control.weapons')}
|
||||
</Segment>
|
||||
|
||||
<Segment
|
||||
groupName="grid"
|
||||
name="summons"
|
||||
selected={props.selectedTab == GridType.Summon}
|
||||
onClick={props.onClick}
|
||||
>{t('party.segmented_control.summons')}</Segment>
|
||||
</SegmentedControl>
|
||||
<Segment
|
||||
groupName="grid"
|
||||
name="summons"
|
||||
selected={props.selectedTab == GridType.Summon}
|
||||
onClick={props.onClick}
|
||||
>
|
||||
{t('party.segmented_control.summons')}
|
||||
</Segment>
|
||||
</SegmentedControl>
|
||||
|
||||
{
|
||||
(() => {
|
||||
if (party.editable && props.selectedTab == GridType.Weapon) {
|
||||
return extraToggle
|
||||
}
|
||||
})()
|
||||
}
|
||||
</div>
|
||||
)
|
||||
{(() => {
|
||||
if (party.editable && props.selectedTab == GridType.Weapon) {
|
||||
return extraToggle
|
||||
}
|
||||
})()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PartySegmentedControl
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
import Select from '~components/Select'
|
||||
import SelectItem from '~components/SelectItem'
|
||||
import SelectGroup from '~components/SelectGroup'
|
||||
|
||||
import api from '~utils/api'
|
||||
import { appState } from '~utils/appState'
|
||||
import { raidGroups } from '~utils/raidGroups'
|
||||
|
|
@ -9,104 +13,136 @@ import './index.scss'
|
|||
|
||||
// Props
|
||||
interface Props {
|
||||
showAllRaidsOption: boolean
|
||||
currentRaid?: string
|
||||
onChange?: (slug?: string) => void
|
||||
onBlur?: (event: React.ChangeEvent<HTMLSelectElement>) => void
|
||||
showAllRaidsOption: boolean
|
||||
currentRaid?: string
|
||||
defaultRaid?: string
|
||||
onChange?: (slug?: string) => void
|
||||
onBlur?: (event: React.ChangeEvent<HTMLSelectElement>) => void
|
||||
}
|
||||
|
||||
const RaidDropdown = React.forwardRef<HTMLSelectElement, Props>(function useFieldSet(props, ref) {
|
||||
const RaidDropdown = React.forwardRef<HTMLSelectElement, Props>(
|
||||
function useFieldSet(props, ref) {
|
||||
// Set up router for locale
|
||||
const router = useRouter()
|
||||
const locale = router.locale || 'en'
|
||||
|
||||
// Set up local states for storing raids
|
||||
const [open, setOpen] = useState(false)
|
||||
const [currentRaid, setCurrentRaid] = useState<Raid>()
|
||||
const [raids, setRaids] = useState<Raid[]>()
|
||||
const [sortedRaids, setSortedRaids] = useState<Raid[][]>()
|
||||
|
||||
function openRaidSelect() {
|
||||
setOpen(!open)
|
||||
}
|
||||
|
||||
// Organize raids into groups on mount
|
||||
const organizeRaids = useCallback((raids: Raid[]) => {
|
||||
const organizeRaids = useCallback(
|
||||
(raids: Raid[]) => {
|
||||
// Set up empty raid for "All raids"
|
||||
const all = {
|
||||
id: '0',
|
||||
name: {
|
||||
en: 'All raids',
|
||||
ja: '全て'
|
||||
},
|
||||
slug: 'all',
|
||||
level: 0,
|
||||
group: 0,
|
||||
element: 0
|
||||
id: '0',
|
||||
name: {
|
||||
en: 'All raids',
|
||||
ja: '全て',
|
||||
},
|
||||
slug: 'all',
|
||||
level: 0,
|
||||
group: 0,
|
||||
element: 0,
|
||||
}
|
||||
|
||||
const numGroups = Math.max.apply(Math, raids.map(raid => raid.group))
|
||||
|
||||
const numGroups = Math.max.apply(
|
||||
Math,
|
||||
raids.map((raid) => raid.group)
|
||||
)
|
||||
let groupedRaids = []
|
||||
|
||||
for (let i = 0; i <= numGroups; i++) {
|
||||
groupedRaids[i] = raids.filter(raid => raid.group == i)
|
||||
groupedRaids[i] = raids.filter((raid) => raid.group == i)
|
||||
}
|
||||
|
||||
if (props.showAllRaidsOption) {
|
||||
raids.unshift(all)
|
||||
groupedRaids[0].unshift(all)
|
||||
raids.unshift(all)
|
||||
groupedRaids[0].unshift(all)
|
||||
}
|
||||
|
||||
setRaids(raids)
|
||||
setSortedRaids(groupedRaids)
|
||||
appState.raids = raids
|
||||
}, [props.showAllRaidsOption])
|
||||
},
|
||||
[props.showAllRaidsOption]
|
||||
)
|
||||
|
||||
// Fetch all raids on mount
|
||||
useEffect(() => {
|
||||
api.endpoints.raids.getAll()
|
||||
.then(response => organizeRaids(response.data.map((r: any) => r.raid)))
|
||||
api.endpoints.raids
|
||||
.getAll()
|
||||
.then((response) =>
|
||||
organizeRaids(response.data.map((r: any) => r.raid))
|
||||
)
|
||||
}, [organizeRaids])
|
||||
|
||||
// Set current raid on mount
|
||||
useEffect(() => {
|
||||
if (raids && props.currentRaid) {
|
||||
const raid = raids.find(raid => raid.slug === props.currentRaid)
|
||||
setCurrentRaid(raid)
|
||||
}
|
||||
if (raids && props.currentRaid) {
|
||||
const raid = raids.find((raid) => raid.slug === props.currentRaid)
|
||||
setCurrentRaid(raid)
|
||||
}
|
||||
}, [raids, props.currentRaid])
|
||||
|
||||
// Enable changing select value
|
||||
function handleChange(event: React.ChangeEvent<HTMLSelectElement>) {
|
||||
if (props.onChange) props.onChange(event.target.value)
|
||||
function handleChange(value: string) {
|
||||
console.log(value)
|
||||
if (props.onChange) props.onChange(value)
|
||||
|
||||
if (raids) {
|
||||
const raid = raids.find(raid => raid.slug === event.target.value)
|
||||
setCurrentRaid(raid)
|
||||
}
|
||||
if (raids) {
|
||||
const raid = raids.find((raid) => raid.slug === value)
|
||||
setCurrentRaid(raid)
|
||||
}
|
||||
}
|
||||
|
||||
// Render JSX for each raid option, sorted into optgroups
|
||||
function renderRaidGroup(index: number) {
|
||||
const options = sortedRaids && sortedRaids.length > 0 && sortedRaids[index].length > 0 &&
|
||||
sortedRaids[index].sort((a, b) => a.element - b.element).map((item, i) => {
|
||||
return (
|
||||
<option key={i} value={item.slug}>{item.name[locale]}</option>
|
||||
)
|
||||
})
|
||||
|
||||
return (
|
||||
<optgroup key={index} label={raidGroups[index].name[locale]}>
|
||||
{options}
|
||||
</optgroup>
|
||||
)
|
||||
const options =
|
||||
sortedRaids &&
|
||||
sortedRaids.length > 0 &&
|
||||
sortedRaids[index].length > 0 &&
|
||||
sortedRaids[index]
|
||||
.sort((a, b) => a.element - b.element)
|
||||
.map((item, i) => {
|
||||
return (
|
||||
<SelectItem key={i} value={item.slug}>
|
||||
{item.name[locale]}
|
||||
</SelectItem>
|
||||
)
|
||||
})
|
||||
return (
|
||||
<SelectGroup
|
||||
key={index}
|
||||
label={raidGroups[index].name[locale]}
|
||||
separator={false}
|
||||
>
|
||||
{options}
|
||||
</SelectGroup>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<select
|
||||
key={currentRaid?.slug}
|
||||
value={currentRaid?.slug}
|
||||
onBlur={props.onBlur}
|
||||
onChange={handleChange}
|
||||
ref={ref}>
|
||||
{ Array.from(Array(sortedRaids?.length)).map((x, i) => renderRaidGroup(i)) }
|
||||
</select>
|
||||
<Select
|
||||
defaultValue={props.defaultRaid}
|
||||
trigger={'Select a raid...'}
|
||||
placeholder={'Select a raid...'}
|
||||
open={open}
|
||||
onClick={openRaidSelect}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{Array.from(Array(sortedRaids?.length)).map((x, i) =>
|
||||
renderRaidGroup(i)
|
||||
)}
|
||||
</Select>
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
export default RaidDropdown
|
||||
|
|
|
|||
|
|
@ -1,74 +1,72 @@
|
|||
.DropdownLabel {
|
||||
align-items: center;
|
||||
background: $grey-90;
|
||||
border: none;
|
||||
border-radius: $unit * 2;
|
||||
color: $grey-40;
|
||||
display: flex;
|
||||
gap: calc($unit / 2);
|
||||
flex-direction: row;
|
||||
padding: ($unit) ($unit * 2);
|
||||
button.DropdownLabel {
|
||||
align-items: center;
|
||||
background: var(--button-contained-bg);
|
||||
border: none;
|
||||
border-radius: $unit-2x;
|
||||
color: var(--text-secondary);
|
||||
display: flex;
|
||||
font-size: $font-small;
|
||||
gap: $unit-half;
|
||||
flex-direction: row;
|
||||
padding: $unit ($unit * 1.5) $unit $unit-2x;
|
||||
|
||||
&:hover {
|
||||
background: $grey-80;
|
||||
color: $grey-00;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.count {
|
||||
color: $grey-60;
|
||||
font-weight: $medium;
|
||||
}
|
||||
|
||||
& > .icon {
|
||||
$diameter: 12px;
|
||||
height: $diameter;
|
||||
width: $diameter;
|
||||
|
||||
svg {
|
||||
transform: scale(0.85);
|
||||
|
||||
path {
|
||||
fill: $grey-60;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
background: var(--button-contained-bg-hover);
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.count {
|
||||
color: var(--text-tertiary);
|
||||
font-weight: $medium;
|
||||
}
|
||||
|
||||
& > .icon {
|
||||
$diameter: 16px;
|
||||
height: $diameter;
|
||||
width: $diameter;
|
||||
|
||||
svg {
|
||||
path {
|
||||
fill: $grey-60;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.Dropdown {
|
||||
background: white;
|
||||
border-radius: $unit;
|
||||
box-shadow: 0 0 2px rgba(0, 0, 0, 0.18);
|
||||
background: var(--button-contained-bg);
|
||||
border-radius: $unit;
|
||||
box-shadow: 0 0 2px rgba(0, 0, 0, 0.18);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: $unit;
|
||||
min-width: 120px;
|
||||
|
||||
& > span {
|
||||
overflow: hidden;
|
||||
|
||||
svg {
|
||||
fill: var(--button-contained-bg);
|
||||
filter: drop-shadow(0px 0px 1px rgb(0 0 0 / 0.18));
|
||||
}
|
||||
}
|
||||
|
||||
section {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: $unit;
|
||||
}
|
||||
|
||||
.Group {
|
||||
flex: 1 1 0px;
|
||||
flex-direction: column;
|
||||
gap: calc($unit / 2);
|
||||
padding: $unit;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
& > span {
|
||||
overflow: hidden;
|
||||
|
||||
svg {
|
||||
fill: white;
|
||||
filter: drop-shadow(0px 0px 1px rgb(0 0 0 / 0.18));
|
||||
}
|
||||
}
|
||||
|
||||
section {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: $unit;
|
||||
}
|
||||
|
||||
.Group {
|
||||
flex: 1 1 0px;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.Label {
|
||||
color: $grey-60;
|
||||
font-size: $font-small;
|
||||
margin-bottom: calc($unit / 2);
|
||||
padding-left: calc($unit / 2);
|
||||
}
|
||||
}
|
||||
.Label {
|
||||
color: var(--text-tertiary);
|
||||
font-size: $font-small;
|
||||
margin-bottom: $unit-half;
|
||||
padding: $unit-half 0 $unit $unit-half;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,29 +6,29 @@ import ArrowIcon from '~public/icons/Arrow.svg'
|
|||
import './index.scss'
|
||||
|
||||
interface Props {
|
||||
label: string
|
||||
open: boolean
|
||||
numSelected: number
|
||||
onOpenChange: (open: boolean) => void
|
||||
children: React.ReactNode
|
||||
label: string
|
||||
open: boolean
|
||||
numSelected: number
|
||||
onOpenChange: (open: boolean) => void
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const SearchFilter = (props: Props) => {
|
||||
return (
|
||||
<DropdownMenu.Root open={props.open} onOpenChange={props.onOpenChange}>
|
||||
<DropdownMenu.Trigger className="DropdownLabel">
|
||||
{props.label}
|
||||
<span className="count">{props.numSelected}</span>
|
||||
<span className="icon">
|
||||
<ArrowIcon />
|
||||
</span>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content className="Dropdown" sideOffset={4}>
|
||||
{props.children}
|
||||
<DropdownMenu.Arrow />
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
)
|
||||
return (
|
||||
<DropdownMenu.Root open={props.open} onOpenChange={props.onOpenChange}>
|
||||
<DropdownMenu.Trigger className="DropdownLabel">
|
||||
{props.label}
|
||||
<span className="count">{props.numSelected}</span>
|
||||
<span className="icon">
|
||||
<ArrowIcon />
|
||||
</span>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content className="Dropdown" sideOffset={4}>
|
||||
{props.children}
|
||||
<DropdownMenu.Arrow />
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export default SearchFilter
|
||||
|
|
|
|||
|
|
@ -1,41 +1,42 @@
|
|||
.Item {
|
||||
align-items: center;
|
||||
border-radius: calc($unit / 2);
|
||||
color: var(--text-secondary);
|
||||
font-size: $font-regular;
|
||||
line-height: 1.2;
|
||||
min-width: 100px;
|
||||
position: relative;
|
||||
padding: $unit;
|
||||
padding-left: $unit * 3.5;
|
||||
|
||||
&:hover {
|
||||
background: var(--button-contained-bg-hover);
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&[data-state='checked'] {
|
||||
background: var(--button-contained-bg-hover);
|
||||
|
||||
svg {
|
||||
fill: var(--text-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.Indicator {
|
||||
$diameter: 20px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: calc($unit / 2);
|
||||
color: $grey-40;
|
||||
font-size: $font-regular;
|
||||
line-height: 1.2;
|
||||
min-width: 100px;
|
||||
position: relative;
|
||||
padding: $unit;
|
||||
padding-left: $unit * 3;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
left: calc($unit / 2);
|
||||
height: $diameter;
|
||||
width: $diameter;
|
||||
|
||||
&:hover {
|
||||
background: $grey-90;
|
||||
cursor: pointer;
|
||||
svg {
|
||||
height: $diameter;
|
||||
width: $diameter;
|
||||
}
|
||||
|
||||
&[data-state="checked"] {
|
||||
background: $grey-90;
|
||||
|
||||
svg {
|
||||
fill: $grey-50;
|
||||
}
|
||||
}
|
||||
|
||||
.Indicator {
|
||||
$diameter: 18px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
left: calc($unit / 2);
|
||||
height: $diameter;
|
||||
width: $diameter;
|
||||
|
||||
svg {
|
||||
height: $diameter;
|
||||
width: $diameter;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,29 +6,30 @@ import CheckIcon from '~public/icons/Check.svg'
|
|||
import './index.scss'
|
||||
|
||||
interface Props {
|
||||
checked?: boolean
|
||||
valueKey: string
|
||||
onCheckedChange: (open: boolean, key: string) => void
|
||||
children: React.ReactNode
|
||||
checked?: boolean
|
||||
valueKey: string
|
||||
onCheckedChange: (open: boolean, key: string) => void
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const SearchFilterCheckboxItem = (props: Props) => {
|
||||
function handleCheckedChange(checked: boolean) {
|
||||
props.onCheckedChange(checked, props.valueKey)
|
||||
}
|
||||
function handleCheckedChange(checked: boolean) {
|
||||
props.onCheckedChange(checked, props.valueKey)
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu.CheckboxItem
|
||||
className="Item"
|
||||
checked={props.checked || false}
|
||||
onCheckedChange={handleCheckedChange}
|
||||
onSelect={ (event) => event.preventDefault() }>
|
||||
<DropdownMenu.ItemIndicator className="Indicator">
|
||||
<CheckIcon />
|
||||
</DropdownMenu.ItemIndicator>
|
||||
{props.children}
|
||||
</DropdownMenu.CheckboxItem>
|
||||
)
|
||||
return (
|
||||
<DropdownMenu.CheckboxItem
|
||||
className="Item"
|
||||
checked={props.checked || false}
|
||||
onCheckedChange={handleCheckedChange}
|
||||
onSelect={(event) => event.preventDefault()}
|
||||
>
|
||||
<DropdownMenu.ItemIndicator className="Indicator">
|
||||
<CheckIcon />
|
||||
</DropdownMenu.ItemIndicator>
|
||||
{props.children}
|
||||
</DropdownMenu.CheckboxItem>
|
||||
)
|
||||
}
|
||||
|
||||
export default SearchFilterCheckboxItem
|
||||
|
|
|
|||
|
|
@ -1,98 +1,99 @@
|
|||
.Search.Dialog {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 431px;
|
||||
width: 600px;
|
||||
height: 480px;
|
||||
gap: 0;
|
||||
padding: 0;
|
||||
|
||||
#Header {
|
||||
border-bottom: 1px solid transparent;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 431px;
|
||||
width: 600px;
|
||||
height: 480px;
|
||||
gap: 0;
|
||||
padding: 0;
|
||||
gap: $unit;
|
||||
padding-bottom: $unit * 2;
|
||||
|
||||
#Header {
|
||||
border-bottom: 1px solid transparent;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit;
|
||||
padding-bottom: $unit * 2;
|
||||
|
||||
&.scrolled {
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
||||
box-shadow: 0 0 8px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
#Bar {
|
||||
border-top-left-radius: $unit;
|
||||
border-top-right-radius: $unit;
|
||||
display: flex;
|
||||
gap: $unit * 2.5;
|
||||
margin: 0;
|
||||
padding: ($unit * 3) ($unit * 3) 0 ($unit * 3);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
|
||||
button {
|
||||
background: transparent;
|
||||
border: none;
|
||||
height: 42px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
width: 100%;
|
||||
|
||||
.Input {
|
||||
background: $grey-90;
|
||||
border: none;
|
||||
border-radius: calc($unit / 2);
|
||||
box-sizing: border-box;
|
||||
font-size: $font-regular;
|
||||
padding: $unit * 1.5;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.scrolled {
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
||||
box-shadow: 0 0 8px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
#Results {
|
||||
margin: 0;
|
||||
max-height: 356px;
|
||||
padding: 0 ($unit * 1.5);
|
||||
overflow-y: scroll;
|
||||
#Bar {
|
||||
align-items: center;
|
||||
border-top-left-radius: $unit;
|
||||
border-top-right-radius: $unit;
|
||||
display: flex;
|
||||
gap: $unit * 2.5;
|
||||
margin: 0;
|
||||
padding: ($unit * 3) ($unit * 3) 0 ($unit * 3);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
|
||||
h5.total {
|
||||
font-size: $font-regular;
|
||||
font-weight: $normal;
|
||||
color: $grey-40;
|
||||
padding: calc($unit / 2) ($unit * 1.5);
|
||||
}
|
||||
button {
|
||||
background: transparent;
|
||||
border: none;
|
||||
height: 42px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.footer {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
color: $grey-60;
|
||||
font-size: $font-regular;
|
||||
font-weight: $normal;
|
||||
height: $unit * 10;
|
||||
justify-content: center;
|
||||
}
|
||||
label {
|
||||
width: 100%;
|
||||
|
||||
.WeaponResult:last-child {
|
||||
margin-bottom: $unit * 1.5;
|
||||
}
|
||||
// .Input {
|
||||
// background: $grey-90;
|
||||
// border: none;
|
||||
// border-radius: calc($unit / 2);
|
||||
// box-sizing: border-box;
|
||||
// font-size: $font-regular;
|
||||
// padding: $unit * 1.5;
|
||||
// text-align: left;
|
||||
// width: 100%;
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Results {
|
||||
margin: 0;
|
||||
max-height: 356px;
|
||||
padding: 0 ($unit * 1.5);
|
||||
overflow-y: scroll;
|
||||
|
||||
h5.total {
|
||||
font-size: $font-regular;
|
||||
font-weight: $normal;
|
||||
color: var(--text-tertiary);
|
||||
padding: $unit-half ($unit * 1.5);
|
||||
}
|
||||
|
||||
.footer {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
color: var(--text-tertiary);
|
||||
font-size: $font-regular;
|
||||
font-weight: $normal;
|
||||
height: $unit-10x;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.WeaponResult:last-child {
|
||||
margin-bottom: $unit * 1.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.Search.Dialog #NoResults {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.Search.Dialog #NoResults h2 {
|
||||
color: #ccc;
|
||||
font-size: $font-large;
|
||||
font-weight: 500;
|
||||
margin-top: -32px;
|
||||
}
|
||||
color: var(--text-secondary);
|
||||
font-size: $font-large;
|
||||
font-weight: 500;
|
||||
margin-top: $unit-4x * -1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,35 +1,41 @@
|
|||
import React, { useEffect, useState } from "react"
|
||||
import { getCookie, setCookie } from "cookies-next"
|
||||
import { useRouter } from "next/router"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import InfiniteScroll from "react-infinite-scroll-component"
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { getCookie, setCookie } from 'cookies-next'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import InfiniteScroll from 'react-infinite-scroll-component'
|
||||
|
||||
import api from "~utils/api"
|
||||
import api from '~utils/api'
|
||||
|
||||
import * as Dialog from "@radix-ui/react-dialog"
|
||||
import {
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
DialogContent,
|
||||
DialogClose,
|
||||
} from '~components/Dialog'
|
||||
|
||||
import CharacterSearchFilterBar from "~components/CharacterSearchFilterBar"
|
||||
import WeaponSearchFilterBar from "~components/WeaponSearchFilterBar"
|
||||
import SummonSearchFilterBar from "~components/SummonSearchFilterBar"
|
||||
import JobSkillSearchFilterBar from "~components/JobSkillSearchFilterBar"
|
||||
import Input from '~components/Input'
|
||||
import CharacterSearchFilterBar from '~components/CharacterSearchFilterBar'
|
||||
import WeaponSearchFilterBar from '~components/WeaponSearchFilterBar'
|
||||
import SummonSearchFilterBar from '~components/SummonSearchFilterBar'
|
||||
import JobSkillSearchFilterBar from '~components/JobSkillSearchFilterBar'
|
||||
|
||||
import CharacterResult from "~components/CharacterResult"
|
||||
import WeaponResult from "~components/WeaponResult"
|
||||
import SummonResult from "~components/SummonResult"
|
||||
import JobSkillResult from "~components/JobSkillResult"
|
||||
import CharacterResult from '~components/CharacterResult'
|
||||
import WeaponResult from '~components/WeaponResult'
|
||||
import SummonResult from '~components/SummonResult'
|
||||
import JobSkillResult from '~components/JobSkillResult'
|
||||
|
||||
import type { SearchableObject, SearchableObjectArray } from "~types"
|
||||
import type { SearchableObject, SearchableObjectArray } from '~types'
|
||||
|
||||
import "./index.scss"
|
||||
import CrossIcon from "~public/icons/Cross.svg"
|
||||
import cloneDeep from "lodash.clonedeep"
|
||||
import './index.scss'
|
||||
import CrossIcon from '~public/icons/Cross.svg'
|
||||
import cloneDeep from 'lodash.clonedeep'
|
||||
|
||||
interface Props {
|
||||
send: (object: SearchableObject, position: number) => any
|
||||
placeholderText: string
|
||||
fromPosition: number
|
||||
job?: Job
|
||||
object: "weapons" | "characters" | "summons" | "job_skills"
|
||||
object: 'weapons' | 'characters' | 'summons' | 'job_skills'
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
|
|
@ -39,7 +45,7 @@ const SearchModal = (props: Props) => {
|
|||
const locale = router.locale
|
||||
|
||||
// Set up translation
|
||||
const { t } = useTranslation("common")
|
||||
const { t } = useTranslation('common')
|
||||
|
||||
let searchInput = React.createRef<HTMLInputElement>()
|
||||
let scrollContainer = React.createRef<HTMLDivElement>()
|
||||
|
|
@ -47,7 +53,7 @@ const SearchModal = (props: Props) => {
|
|||
const [firstLoad, setFirstLoad] = useState(true)
|
||||
const [filters, setFilters] = useState<{ [key: string]: any }>()
|
||||
const [open, setOpen] = useState(false)
|
||||
const [query, setQuery] = useState("")
|
||||
const [query, setQuery] = useState('')
|
||||
const [results, setResults] = useState<SearchableObjectArray>([])
|
||||
|
||||
// Pagination states
|
||||
|
|
@ -64,7 +70,7 @@ const SearchModal = (props: Props) => {
|
|||
if (text.length) {
|
||||
setQuery(text)
|
||||
} else {
|
||||
setQuery("")
|
||||
setQuery('')
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -113,7 +119,7 @@ const SearchModal = (props: Props) => {
|
|||
: []
|
||||
let recents: SearchableObjectArray = []
|
||||
|
||||
if (props.object === "weapons") {
|
||||
if (props.object === 'weapons') {
|
||||
recents = cloneDeep(cookieObj as Weapon[]) || []
|
||||
if (
|
||||
!recents.find(
|
||||
|
|
@ -123,7 +129,7 @@ const SearchModal = (props: Props) => {
|
|||
) {
|
||||
recents.unshift(result as Weapon)
|
||||
}
|
||||
} else if (props.object === "summons") {
|
||||
} else if (props.object === 'summons') {
|
||||
recents = cloneDeep(cookieObj as Summon[]) || []
|
||||
if (
|
||||
!recents.find(
|
||||
|
|
@ -136,7 +142,7 @@ const SearchModal = (props: Props) => {
|
|||
}
|
||||
|
||||
if (recents && recents.length > 5) recents.pop()
|
||||
setCookie(`recent_${props.object}`, recents, { path: "/" })
|
||||
setCookie(`recent_${props.object}`, recents, { path: '/' })
|
||||
sendData(result)
|
||||
}
|
||||
|
||||
|
|
@ -192,16 +198,16 @@ const SearchModal = (props: Props) => {
|
|||
let jsx
|
||||
|
||||
switch (props.object) {
|
||||
case "weapons":
|
||||
case 'weapons':
|
||||
jsx = renderWeaponSearchResults()
|
||||
break
|
||||
case "summons":
|
||||
case 'summons':
|
||||
jsx = renderSummonSearchResults(results)
|
||||
break
|
||||
case "characters":
|
||||
case 'characters':
|
||||
jsx = renderCharacterSearchResults(results)
|
||||
break
|
||||
case "job_skills":
|
||||
case 'job_skills':
|
||||
jsx = renderJobSkillSearchResults(results)
|
||||
break
|
||||
}
|
||||
|
|
@ -305,7 +311,7 @@ const SearchModal = (props: Props) => {
|
|||
|
||||
function openChange() {
|
||||
if (open) {
|
||||
setQuery("")
|
||||
setQuery('')
|
||||
setFirstLoad(true)
|
||||
setResults([])
|
||||
setRecordCount(0)
|
||||
|
|
@ -317,61 +323,54 @@ const SearchModal = (props: Props) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<Dialog.Root open={open} onOpenChange={openChange}>
|
||||
<Dialog.Trigger asChild>{props.children}</Dialog.Trigger>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Content className="Search Dialog">
|
||||
<div id="Header">
|
||||
<div id="Bar">
|
||||
<label className="search_label" htmlFor="search_input">
|
||||
<input
|
||||
autoComplete="off"
|
||||
type="text"
|
||||
name="query"
|
||||
className="Input"
|
||||
id="search_input"
|
||||
ref={searchInput}
|
||||
value={query}
|
||||
placeholder={props.placeholderText}
|
||||
onChange={inputChanged}
|
||||
/>
|
||||
</label>
|
||||
<Dialog.Close className="DialogClose" onClick={openChange}>
|
||||
<CrossIcon />
|
||||
</Dialog.Close>
|
||||
</div>
|
||||
{props.object === "characters" ? (
|
||||
<CharacterSearchFilterBar sendFilters={receiveFilters} />
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
{props.object === "weapons" ? (
|
||||
<WeaponSearchFilterBar sendFilters={receiveFilters} />
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
{props.object === "summons" ? (
|
||||
<SummonSearchFilterBar sendFilters={receiveFilters} />
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
{props.object === "job_skills" ? (
|
||||
<JobSkillSearchFilterBar sendFilters={receiveFilters} />
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
<Dialog open={open} onOpenChange={openChange}>
|
||||
<DialogTrigger asChild>{props.children}</DialogTrigger>
|
||||
<DialogContent className="Search Dialog">
|
||||
<div id="Header">
|
||||
<div id="Bar">
|
||||
<Input
|
||||
autoComplete="off"
|
||||
className="Search"
|
||||
name="query"
|
||||
placeholder={props.placeholderText}
|
||||
ref={searchInput}
|
||||
value={query}
|
||||
onChange={inputChanged}
|
||||
/>
|
||||
<DialogClose className="DialogClose" onClick={openChange}>
|
||||
<CrossIcon />
|
||||
</DialogClose>
|
||||
</div>
|
||||
{props.object === 'characters' ? (
|
||||
<CharacterSearchFilterBar sendFilters={receiveFilters} />
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
{props.object === 'weapons' ? (
|
||||
<WeaponSearchFilterBar sendFilters={receiveFilters} />
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
{props.object === 'summons' ? (
|
||||
<SummonSearchFilterBar sendFilters={receiveFilters} />
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
{props.object === 'job_skills' ? (
|
||||
<JobSkillSearchFilterBar sendFilters={receiveFilters} />
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div id="Results" ref={scrollContainer}>
|
||||
<h5 className="total">
|
||||
{t("search.result_count", { record_count: recordCount })}
|
||||
</h5>
|
||||
{open ? renderResults() : ""}
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
<Dialog.Overlay className="Overlay" />
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
<div id="Results" ref={scrollContainer}>
|
||||
<h5 className="total">
|
||||
{t('search.result_count', { record_count: recordCount })}
|
||||
</h5>
|
||||
{open ? renderResults() : ''}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,36 +1,36 @@
|
|||
.Segment {
|
||||
color: $grey-50;
|
||||
color: $grey-55;
|
||||
cursor: pointer;
|
||||
font-size: 1.4rem;
|
||||
font-weight: $normal;
|
||||
min-width: 100px;
|
||||
|
||||
&:hover label {
|
||||
background: var(--page-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
& input {
|
||||
display: none;
|
||||
|
||||
&:checked + label {
|
||||
background: var(--background);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
|
||||
& label {
|
||||
border-radius: $unit * 3;
|
||||
display: block;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
padding: 8px 12px;
|
||||
text-overflow: ellipsis;
|
||||
cursor: pointer;
|
||||
font-size: 1.4rem;
|
||||
font-weight: $normal;
|
||||
min-width: 100px;
|
||||
|
||||
&:hover label {
|
||||
background: $grey-90;
|
||||
color: $grey-40;
|
||||
&:before {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
& input {
|
||||
display: none;
|
||||
|
||||
&:checked + label {
|
||||
background: $grey-90;
|
||||
color: $grey-00;
|
||||
}
|
||||
}
|
||||
|
||||
& label {
|
||||
border-radius: $unit * 3;
|
||||
display: block;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
padding: 8px 12px;
|
||||
text-overflow: ellipsis;
|
||||
cursor: pointer;
|
||||
|
||||
&:before {
|
||||
background: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,31 +3,27 @@ import React from 'react'
|
|||
import './index.scss'
|
||||
|
||||
interface Props {
|
||||
groupName: string
|
||||
name: string
|
||||
selected: boolean
|
||||
children: string
|
||||
onClick: (event: React.ChangeEvent<HTMLInputElement>) => void
|
||||
groupName: string
|
||||
name: string
|
||||
selected: boolean
|
||||
children: string
|
||||
onClick: (event: React.ChangeEvent<HTMLInputElement>) => void
|
||||
}
|
||||
|
||||
const Segment: React.FC<Props> = (props: Props) => {
|
||||
|
||||
|
||||
return (
|
||||
<div className="Segment">
|
||||
<input
|
||||
name={props.groupName}
|
||||
id={props.name}
|
||||
value={props.name}
|
||||
type="radio"
|
||||
checked={props.selected}
|
||||
onChange={props.onClick}
|
||||
/>
|
||||
<label htmlFor={props.name}>
|
||||
{props.children}
|
||||
</label>
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<div className="Segment">
|
||||
<input
|
||||
name={props.groupName}
|
||||
id={props.name}
|
||||
value={props.name}
|
||||
type="radio"
|
||||
checked={props.selected}
|
||||
onChange={props.onClick}
|
||||
/>
|
||||
<label htmlFor={props.name}>{props.children}</label>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Segment
|
||||
export default Segment
|
||||
|
|
|
|||
|
|
@ -1,88 +1,87 @@
|
|||
.SegmentedControlWrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.SegmentedControl {
|
||||
background: white;
|
||||
border-radius: $unit * 3;
|
||||
display: inline-flex;
|
||||
padding: 3px;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
overflow: hidden;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
z-index: 1;
|
||||
|
||||
&.fire {
|
||||
.Segment input:checked + label {
|
||||
background: $fire-bg-dark;
|
||||
color: $fire-text-dark;
|
||||
}
|
||||
background: var(--card-bg);
|
||||
border-radius: $unit * 3;
|
||||
display: inline-flex;
|
||||
padding: 3px;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
overflow: hidden;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
|
||||
.Segment:hover label {
|
||||
background: $fire-bg-light;
|
||||
color: $fire-text-light;
|
||||
}
|
||||
&.fire {
|
||||
.Segment input:checked + label {
|
||||
background: $fire-bg-10;
|
||||
color: $fire-text-10;
|
||||
}
|
||||
|
||||
&.water {
|
||||
.Segment input:checked + label {
|
||||
background: $water-bg-dark;
|
||||
color: $water-text-dark;
|
||||
}
|
||||
.Segment:hover label {
|
||||
background: var(--fire-hover-bg);
|
||||
color: var(--fire-hover-text);
|
||||
}
|
||||
}
|
||||
|
||||
.Segment:hover label {
|
||||
background: $water-bg-light;
|
||||
color: $water-text-light;
|
||||
}
|
||||
&.water {
|
||||
.Segment input:checked + label {
|
||||
background: $water-bg-10;
|
||||
color: $water-text-10;
|
||||
}
|
||||
|
||||
&.earth {
|
||||
.Segment input:checked + label {
|
||||
background: $earth-bg-dark;
|
||||
color: $earth-text-dark;
|
||||
}
|
||||
.Segment:hover label {
|
||||
background: var(--water-hover-bg);
|
||||
color: var(--water-hover-text);
|
||||
}
|
||||
}
|
||||
|
||||
.Segment:hover label {
|
||||
background: $earth-bg-light;
|
||||
color: $earth-text-light;
|
||||
}
|
||||
&.earth {
|
||||
.Segment input:checked + label {
|
||||
background: $earth-bg-10;
|
||||
color: $earth-text-10;
|
||||
}
|
||||
|
||||
&.wind {
|
||||
.Segment input:checked + label {
|
||||
background: $wind-bg-dark;
|
||||
color: $wind-text-dark;
|
||||
}
|
||||
.Segment:hover label {
|
||||
background: var(--earth-hover-bg);
|
||||
color: var(--earth-hover-text);
|
||||
}
|
||||
}
|
||||
|
||||
.Segment:hover label {
|
||||
background: $wind-bg-light;
|
||||
color: $wind-text-light;
|
||||
}
|
||||
&.wind {
|
||||
.Segment input:checked + label {
|
||||
background: $wind-bg-10;
|
||||
color: $wind-text-10;
|
||||
}
|
||||
|
||||
&.light {
|
||||
.Segment input:checked + label {
|
||||
background: $light-bg-dark;
|
||||
color: $light-text-dark;
|
||||
}
|
||||
.Segment:hover label {
|
||||
background: var(--wind-hover-bg);
|
||||
color: var(--wind-hover-text);
|
||||
}
|
||||
}
|
||||
|
||||
.Segment:hover label {
|
||||
background: $light-bg-light;
|
||||
color: $light-text-light;
|
||||
}
|
||||
&.light {
|
||||
.Segment input:checked + label {
|
||||
background: $light-bg-10;
|
||||
color: $light-text-10;
|
||||
}
|
||||
|
||||
&.dark {
|
||||
.Segment input:checked + label {
|
||||
background: $dark-bg-dark;
|
||||
color: $dark-text-dark;
|
||||
}
|
||||
|
||||
.Segment:hover label {
|
||||
background: $dark-bg-light;
|
||||
color: $dark-text-light;
|
||||
}
|
||||
.Segment:hover label {
|
||||
background: var(--light-hover-bg);
|
||||
color: var(--light-hover-text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.dark {
|
||||
.Segment input:checked + label {
|
||||
background: $dark-bg-10;
|
||||
color: $dark-text-10;
|
||||
}
|
||||
|
||||
.Segment:hover label {
|
||||
background: var(--dark-hover-bg);
|
||||
color: var(--dark-hover-text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,17 +3,17 @@ import React from 'react'
|
|||
import './index.scss'
|
||||
|
||||
interface Props {
|
||||
elementClass?: string
|
||||
elementClass?: string
|
||||
}
|
||||
|
||||
const SegmentedControl: React.FC<Props> = ({ elementClass, children }) => {
|
||||
return (
|
||||
<div className="SegmentedControlWrapper">
|
||||
<div className={`SegmentedControl ${(elementClass) ? elementClass : ''}`}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<div className="SegmentedControlWrapper">
|
||||
<div className={`SegmentedControl ${elementClass ? elementClass : ''}`}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SegmentedControl
|
||||
export default SegmentedControl
|
||||
|
|
|
|||
60
components/Select/index.scss
Normal file
60
components/Select/index.scss
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
.SelectTrigger {
|
||||
align-items: center;
|
||||
background-color: var(--input-bg);
|
||||
border-radius: $input-corner;
|
||||
border: none;
|
||||
display: flex;
|
||||
padding: $unit-2x $unit-2x;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--input-bg-hover);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&[data-placeholder] > span:not(.SelectIcon) {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
& > span:not(.SelectIcon) {
|
||||
color: var(--text-primary);
|
||||
flex-grow: 1;
|
||||
font-size: $font-regular;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.SelectIcon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
svg {
|
||||
fill: var(--icon-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.Select {
|
||||
background: var(--select-bg);
|
||||
border-radius: $input-corner;
|
||||
border: $hover-stroke;
|
||||
box-shadow: $hover-shadow;
|
||||
padding: 0 $unit;
|
||||
z-index: 40;
|
||||
|
||||
.Scroll.Up,
|
||||
.Scroll.Down {
|
||||
padding: $unit 0;
|
||||
text-align: center;
|
||||
|
||||
&:hover svg {
|
||||
fill: var(--icon-secondary-hover);
|
||||
}
|
||||
|
||||
svg {
|
||||
fill: var(--icon-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.Scroll.Up {
|
||||
transform: scale(1, -1);
|
||||
}
|
||||
}
|
||||
55
components/Select/index.tsx
Normal file
55
components/Select/index.tsx
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import React from 'react'
|
||||
import * as RadixSelect from '@radix-ui/react-select'
|
||||
import classNames from 'classnames'
|
||||
|
||||
import ArrowIcon from '~public/icons/Arrow.svg'
|
||||
|
||||
import './index.scss'
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
open: boolean
|
||||
defaultValue?: string | number
|
||||
placeholder?: string
|
||||
trigger?: React.ReactNode
|
||||
children?: React.ReactNode
|
||||
onClick?: () => void
|
||||
onChange?: (value: string) => void
|
||||
triggerClass?: string
|
||||
}
|
||||
|
||||
const Select = React.forwardRef<HTMLSelectElement, Props>(function useFieldSet(
|
||||
props,
|
||||
ref
|
||||
) {
|
||||
return (
|
||||
<RadixSelect.Root
|
||||
defaultValue={props.defaultValue as string}
|
||||
onValueChange={props.onChange}
|
||||
>
|
||||
<RadixSelect.Trigger
|
||||
className={classNames('SelectTrigger', props.triggerClass)}
|
||||
placeholder={props.placeholder}
|
||||
>
|
||||
<RadixSelect.Value placeholder={props.placeholder} />
|
||||
<RadixSelect.Icon className="SelectIcon">
|
||||
<ArrowIcon />
|
||||
</RadixSelect.Icon>
|
||||
</RadixSelect.Trigger>
|
||||
|
||||
<RadixSelect.Portal className="Select">
|
||||
<RadixSelect.Content>
|
||||
<RadixSelect.ScrollUpButton className="Scroll Up">
|
||||
<ArrowIcon />
|
||||
</RadixSelect.ScrollUpButton>
|
||||
<RadixSelect.Viewport>{props.children}</RadixSelect.Viewport>
|
||||
<RadixSelect.ScrollDownButton className="Scroll Down">
|
||||
<ArrowIcon />
|
||||
</RadixSelect.ScrollDownButton>
|
||||
</RadixSelect.Content>
|
||||
</RadixSelect.Portal>
|
||||
</RadixSelect.Root>
|
||||
)
|
||||
})
|
||||
|
||||
export default Select
|
||||
25
components/SelectGroup/index.scss
Normal file
25
components/SelectGroup/index.scss
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
.SelectGroup {
|
||||
.Label {
|
||||
align-items: center;
|
||||
color: var(--text-tertiary);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-shrink: 0;
|
||||
font-size: $font-small;
|
||||
font-weight: $medium;
|
||||
gap: $unit;
|
||||
padding: $unit $unit-2x $unit-half;
|
||||
|
||||
&:first-child {
|
||||
padding-top: $unit-2x;
|
||||
}
|
||||
|
||||
.Separator {
|
||||
background: var(--select-separator);
|
||||
border-radius: 1px;
|
||||
display: block;
|
||||
flex-grow: 1;
|
||||
height: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
33
components/SelectGroup/index.tsx
Normal file
33
components/SelectGroup/index.tsx
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import React from 'react'
|
||||
import * as RadixSelect from '@radix-ui/react-select'
|
||||
|
||||
import './index.scss'
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
label?: string
|
||||
separator?: boolean
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
const defaultProps = {
|
||||
separator: true,
|
||||
}
|
||||
|
||||
const SelectGroup = (props: Props) => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<RadixSelect.Group className="SelectGroup">
|
||||
<RadixSelect.Label className="Label">
|
||||
{props.label}
|
||||
<RadixSelect.Separator className="Separator" />
|
||||
</RadixSelect.Label>
|
||||
{props.children}
|
||||
</RadixSelect.Group>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
SelectGroup.defaultProps = defaultProps
|
||||
|
||||
export default SelectGroup
|
||||
27
components/SelectItem/index.scss
Normal file
27
components/SelectItem/index.scss
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
.SelectItem {
|
||||
border-radius: $item-corner;
|
||||
border: 2px solid transparent;
|
||||
color: var(--text-secondary);
|
||||
font-size: $font-regular;
|
||||
padding: ($unit * 1.5) $unit-2x;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--option-bg-hover);
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border: 2px solid $blue;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
margin-top: $unit;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: $unit;
|
||||
}
|
||||
}
|
||||
27
components/SelectItem/index.tsx
Normal file
27
components/SelectItem/index.tsx
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import React, { ComponentProps } from 'react'
|
||||
import * as Select from '@radix-ui/react-select'
|
||||
|
||||
import './index.scss'
|
||||
import classNames from 'classnames'
|
||||
|
||||
interface Props extends ComponentProps<'div'> {
|
||||
value: string | number
|
||||
}
|
||||
|
||||
const SelectItem = React.forwardRef<HTMLDivElement, Props>(function selectItem(
|
||||
{ children, ...props },
|
||||
forwardedRef
|
||||
) {
|
||||
return (
|
||||
<Select.Item
|
||||
className={classNames('SelectItem', props.className)}
|
||||
{...props}
|
||||
ref={forwardedRef}
|
||||
value={`${props.value}`}
|
||||
>
|
||||
<Select.ItemText>{children}</Select.ItemText>
|
||||
</Select.Item>
|
||||
)
|
||||
})
|
||||
|
||||
export default SelectItem
|
||||
|
|
@ -1,47 +1,47 @@
|
|||
.Signup.Dialog form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: calc($unit / 2);
|
||||
margin-bottom: $unit;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: calc($unit / 2);
|
||||
margin-bottom: $unit;
|
||||
|
||||
.Button {
|
||||
font-size: $font-regular;
|
||||
padding: ($unit * 1.5) ($unit * 2);
|
||||
width: 100%;
|
||||
.Button {
|
||||
font-size: $font-regular;
|
||||
padding: ($unit * 1.5) ($unit * 2);
|
||||
width: 100%;
|
||||
|
||||
&.btn-disabled {
|
||||
background: $grey-90;
|
||||
color: $grey-70;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&:not(.btn-disabled) {
|
||||
background: $grey-90;
|
||||
color: $grey-40;
|
||||
|
||||
&:hover {
|
||||
background: $grey-80;
|
||||
}
|
||||
}
|
||||
&.btn-disabled {
|
||||
background: $grey-90;
|
||||
color: $grey-70;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.terms {
|
||||
color: $grey-40;
|
||||
font-size: $font-small;
|
||||
line-height: 1.2;
|
||||
margin-top: $unit;
|
||||
text-align: center;
|
||||
&:not(.btn-disabled) {
|
||||
background: $grey-90;
|
||||
color: $grey-50;
|
||||
|
||||
a {
|
||||
color: $blue;
|
||||
|
||||
&:hover {
|
||||
color: darken($blue, 30);
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
background: $grey-80;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
background: $grey-90;
|
||||
.terms {
|
||||
color: $grey-50;
|
||||
font-size: $font-small;
|
||||
line-height: 1.2;
|
||||
margin-top: $unit;
|
||||
text-align: center;
|
||||
|
||||
a {
|
||||
color: $blue;
|
||||
|
||||
&:hover {
|
||||
color: darken($blue, 30);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
background: $grey-90;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,20 @@
|
|||
import React, { useEffect, useState } from "react"
|
||||
import Link from "next/link"
|
||||
import { setCookie } from "cookies-next"
|
||||
import { useRouter } from "next/router"
|
||||
import { Trans, useTranslation } from "next-i18next"
|
||||
import { AxiosResponse } from "axios"
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { setCookie } from 'cookies-next'
|
||||
import { useRouter } from 'next/router'
|
||||
import { Trans, useTranslation } from 'next-i18next'
|
||||
import { AxiosResponse } from 'axios'
|
||||
|
||||
import * as Dialog from "@radix-ui/react-dialog"
|
||||
import * as Dialog from '@radix-ui/react-dialog'
|
||||
|
||||
import api from "~utils/api"
|
||||
import { accountState } from "~utils/accountState"
|
||||
import api from '~utils/api'
|
||||
import { accountState } from '~utils/accountState'
|
||||
|
||||
import Button from "~components/Button"
|
||||
import Fieldset from "~components/Fieldset"
|
||||
import Button from '~components/Button'
|
||||
import Fieldset from '~components/Input'
|
||||
|
||||
import CrossIcon from "~public/icons/Cross.svg"
|
||||
import "./index.scss"
|
||||
import CrossIcon from '~public/icons/Cross.svg'
|
||||
import './index.scss'
|
||||
|
||||
interface Props {}
|
||||
|
||||
|
|
@ -31,15 +31,15 @@ const emailRegex =
|
|||
|
||||
const SignupModal = (props: Props) => {
|
||||
const router = useRouter()
|
||||
const { t } = useTranslation("common")
|
||||
const { t } = useTranslation('common')
|
||||
|
||||
// Set up form states and error handling
|
||||
const [formValid, setFormValid] = useState(false)
|
||||
const [errors, setErrors] = useState<ErrorMap>({
|
||||
username: "",
|
||||
email: "",
|
||||
password: "",
|
||||
passwordConfirmation: "",
|
||||
username: '',
|
||||
email: '',
|
||||
password: '',
|
||||
passwordConfirmation: '',
|
||||
})
|
||||
|
||||
// States
|
||||
|
|
@ -90,7 +90,7 @@ const SignupModal = (props: Props) => {
|
|||
token: user.token,
|
||||
}
|
||||
|
||||
setCookie("account", cookieObj, { path: "/" })
|
||||
setCookie('account', cookieObj, { path: '/' })
|
||||
}
|
||||
|
||||
function fetchUserInfo(id: string) {
|
||||
|
|
@ -108,7 +108,7 @@ const SignupModal = (props: Props) => {
|
|||
}
|
||||
|
||||
// TODO: Set language
|
||||
setCookie("user", cookieObj, { path: "/" })
|
||||
setCookie('user', cookieObj, { path: '/' })
|
||||
|
||||
accountState.account.user = {
|
||||
id: user.id,
|
||||
|
|
@ -151,13 +151,13 @@ const SignupModal = (props: Props) => {
|
|||
|
||||
if (available) {
|
||||
// Continue checking for errors
|
||||
newErrors[fieldName] = ""
|
||||
newErrors[fieldName] = ''
|
||||
setErrors(newErrors)
|
||||
setFormValid(true)
|
||||
|
||||
validateName(fieldName, value)
|
||||
} else {
|
||||
newErrors[fieldName] = t("modals.signup.errors.field_in_use", {
|
||||
newErrors[fieldName] = t('modals.signup.errors.field_in_use', {
|
||||
field: fieldName,
|
||||
})
|
||||
setErrors(newErrors)
|
||||
|
|
@ -169,19 +169,19 @@ const SignupModal = (props: Props) => {
|
|||
let newErrors = { ...errors }
|
||||
|
||||
switch (fieldName) {
|
||||
case "username":
|
||||
case 'username':
|
||||
if (value.length < 3)
|
||||
newErrors.username = t("modals.signup.errors.username_too_short")
|
||||
newErrors.username = t('modals.signup.errors.username_too_short')
|
||||
else if (value.length > 20)
|
||||
newErrors.username = t("modals.signup.errors.username_too_long")
|
||||
else newErrors.username = ""
|
||||
newErrors.username = t('modals.signup.errors.username_too_long')
|
||||
else newErrors.username = ''
|
||||
|
||||
break
|
||||
|
||||
case "email":
|
||||
case 'email':
|
||||
newErrors.email = emailRegex.test(value)
|
||||
? ""
|
||||
: t("modals.signup.errors.invalid_email")
|
||||
? ''
|
||||
: t('modals.signup.errors.invalid_email')
|
||||
break
|
||||
|
||||
default:
|
||||
|
|
@ -198,25 +198,25 @@ const SignupModal = (props: Props) => {
|
|||
let newErrors = { ...errors }
|
||||
|
||||
switch (name) {
|
||||
case "password":
|
||||
case 'password':
|
||||
newErrors.password = passwordInput.current?.value.includes(
|
||||
usernameInput.current?.value!
|
||||
)
|
||||
? t("modals.signup.errors.password_contains_username")
|
||||
: ""
|
||||
? t('modals.signup.errors.password_contains_username')
|
||||
: ''
|
||||
break
|
||||
|
||||
case "password":
|
||||
case 'password':
|
||||
newErrors.password =
|
||||
value.length < 8 ? t("modals.signup.errors.password_too_short") : ""
|
||||
value.length < 8 ? t('modals.signup.errors.password_too_short') : ''
|
||||
break
|
||||
|
||||
case "confirm_password":
|
||||
case 'confirm_password':
|
||||
newErrors.passwordConfirmation =
|
||||
passwordInput.current?.value ===
|
||||
passwordConfirmationInput.current?.value
|
||||
? ""
|
||||
: t("modals.signup.errors.passwords_dont_match")
|
||||
? ''
|
||||
: t('modals.signup.errors.passwords_dont_match')
|
||||
break
|
||||
|
||||
default:
|
||||
|
|
@ -243,10 +243,10 @@ const SignupModal = (props: Props) => {
|
|||
function openChange(open: boolean) {
|
||||
setOpen(open)
|
||||
setErrors({
|
||||
username: "",
|
||||
email: "",
|
||||
password: "",
|
||||
passwordConfirmation: "",
|
||||
username: '',
|
||||
email: '',
|
||||
password: '',
|
||||
passwordConfirmation: '',
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -254,7 +254,7 @@ const SignupModal = (props: Props) => {
|
|||
<Dialog.Root open={open} onOpenChange={openChange}>
|
||||
<Dialog.Trigger asChild>
|
||||
<li className="MenuItem">
|
||||
<span>{t("menu.signup")}</span>
|
||||
<span>{t('menu.signup')}</span>
|
||||
</li>
|
||||
</Dialog.Trigger>
|
||||
<Dialog.Portal>
|
||||
|
|
@ -264,7 +264,7 @@ const SignupModal = (props: Props) => {
|
|||
>
|
||||
<div className="DialogHeader">
|
||||
<Dialog.Title className="DialogTitle">
|
||||
{t("modals.signup.title")}
|
||||
{t('modals.signup.title')}
|
||||
</Dialog.Title>
|
||||
<Dialog.Close className="DialogClose" asChild>
|
||||
<span>
|
||||
|
|
@ -276,7 +276,7 @@ const SignupModal = (props: Props) => {
|
|||
<form className="form" onSubmit={register}>
|
||||
<Fieldset
|
||||
fieldName="username"
|
||||
placeholder={t("modals.signup.placeholders.username")}
|
||||
placeholder={t('modals.signup.placeholders.username')}
|
||||
onChange={handleNameChange}
|
||||
error={errors.username}
|
||||
ref={usernameInput}
|
||||
|
|
@ -284,7 +284,7 @@ const SignupModal = (props: Props) => {
|
|||
|
||||
<Fieldset
|
||||
fieldName="email"
|
||||
placeholder={t("modals.signup.placeholders.email")}
|
||||
placeholder={t('modals.signup.placeholders.email')}
|
||||
onChange={handleNameChange}
|
||||
error={errors.email}
|
||||
ref={emailInput}
|
||||
|
|
@ -292,7 +292,7 @@ const SignupModal = (props: Props) => {
|
|||
|
||||
<Fieldset
|
||||
fieldName="password"
|
||||
placeholder={t("modals.signup.placeholders.password")}
|
||||
placeholder={t('modals.signup.placeholders.password')}
|
||||
onChange={handlePasswordChange}
|
||||
error={errors.password}
|
||||
ref={passwordInput}
|
||||
|
|
@ -300,13 +300,13 @@ const SignupModal = (props: Props) => {
|
|||
|
||||
<Fieldset
|
||||
fieldName="confirm_password"
|
||||
placeholder={t("modals.signup.placeholders.password_confirm")}
|
||||
placeholder={t('modals.signup.placeholders.password_confirm')}
|
||||
onChange={handlePasswordChange}
|
||||
error={errors.passwordConfirmation}
|
||||
ref={passwordConfirmationInput}
|
||||
/>
|
||||
|
||||
<Button>{t("modals.signup.buttons.confirm")}</Button>
|
||||
<Button>{t('modals.signup.buttons.confirm')}</Button>
|
||||
|
||||
<Dialog.Description className="terms">
|
||||
{/* <Trans i18nKey="modals.signup.agreement">
|
||||
|
|
|
|||
|
|
@ -1,26 +1,26 @@
|
|||
#SummonGrid {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto auto;
|
||||
grid-column-gap: $unit * 2;
|
||||
justify-content: center;
|
||||
|
||||
& .Label {
|
||||
color: $grey-55;
|
||||
font-size: $font-tiny;
|
||||
font-weight: $medium;
|
||||
margin-bottom: $unit;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#grid_summons {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto auto;
|
||||
grid-template-columns: auto auto;
|
||||
grid-column-gap: $unit * 2;
|
||||
justify-content: center;
|
||||
grid-template-rows: 1fr;
|
||||
grid-row-gap: $unit * 3;
|
||||
|
||||
& .Label {
|
||||
color: $grey-50;
|
||||
font-size: $font-tiny;
|
||||
font-weight: $medium;
|
||||
margin-bottom: $unit;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#grid_summons {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
grid-column-gap: $unit * 2;
|
||||
grid-template-rows: 1fr;
|
||||
grid-row-gap: $unit * 3;
|
||||
|
||||
& > li {
|
||||
list-style: none;
|
||||
}
|
||||
& > li {
|
||||
list-style: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,20 @@
|
|||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react"
|
||||
import { getCookie } from "cookies-next"
|
||||
import { useSnapshot } from "valtio"
|
||||
import { useTranslation } from "next-i18next"
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { getCookie } from 'cookies-next'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
|
||||
import { AxiosResponse } from "axios"
|
||||
import debounce from "lodash.debounce"
|
||||
import { AxiosResponse } from 'axios'
|
||||
import debounce from 'lodash.debounce'
|
||||
|
||||
import SummonUnit from "~components/SummonUnit"
|
||||
import ExtraSummons from "~components/ExtraSummons"
|
||||
import SummonUnit from '~components/SummonUnit'
|
||||
import ExtraSummons from '~components/ExtraSummons'
|
||||
|
||||
import api from "~utils/api"
|
||||
import { appState } from "~utils/appState"
|
||||
import type { SearchableObject } from "~types"
|
||||
import api from '~utils/api'
|
||||
import { appState } from '~utils/appState'
|
||||
import type { SearchableObject } from '~types'
|
||||
|
||||
import "./index.scss"
|
||||
import './index.scss'
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
|
|
@ -29,7 +29,7 @@ const SummonGrid = (props: Props) => {
|
|||
const numSummons: number = 4
|
||||
|
||||
// Cookies
|
||||
const cookie = getCookie("account")
|
||||
const cookie = getCookie('account')
|
||||
const accountData: AccountCookie = cookie
|
||||
? JSON.parse(cookie as string)
|
||||
: null
|
||||
|
|
@ -38,7 +38,7 @@ const SummonGrid = (props: Props) => {
|
|||
: {}
|
||||
|
||||
// Localization
|
||||
const { t } = useTranslation("common")
|
||||
const { t } = useTranslation('common')
|
||||
|
||||
// Set up state for view management
|
||||
const { party, grid } = useSnapshot(appState)
|
||||
|
|
@ -141,7 +141,7 @@ const SummonGrid = (props: Props) => {
|
|||
|
||||
try {
|
||||
if (uncapLevel != previousUncapValues[position])
|
||||
await api.updateUncap("summon", id, uncapLevel).then((response) => {
|
||||
await api.updateUncap('summon', id, uncapLevel).then((response) => {
|
||||
storeGridSummon(response.data.grid_summon)
|
||||
})
|
||||
} catch (error) {
|
||||
|
|
@ -217,7 +217,7 @@ const SummonGrid = (props: Props) => {
|
|||
// Render: JSX components
|
||||
const mainSummonElement = (
|
||||
<div className="LabeledUnit">
|
||||
<div className="Label">{t("summons.main")}</div>
|
||||
<div className="Label">{t('summons.main')}</div>
|
||||
<SummonUnit
|
||||
gridSummon={grid.summons.mainSummon}
|
||||
editable={party.editable}
|
||||
|
|
@ -232,7 +232,7 @@ const SummonGrid = (props: Props) => {
|
|||
|
||||
const friendSummonElement = (
|
||||
<div className="LabeledUnit">
|
||||
<div className="Label">{t("summons.friend")}</div>
|
||||
<div className="Label">{t('summons.friend')}</div>
|
||||
<SummonUnit
|
||||
gridSummon={grid.summons.friendSummon}
|
||||
editable={party.editable}
|
||||
|
|
@ -246,7 +246,7 @@ const SummonGrid = (props: Props) => {
|
|||
)
|
||||
const summonGridElement = (
|
||||
<div id="LabeledGrid">
|
||||
<div className="Label">{t("summons.summons")}</div>
|
||||
<div className="Label">{t('summons.summons')}</div>
|
||||
<ul id="grid_summons">
|
||||
{Array.from(Array(numSummons)).map((x, i) => {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -10,71 +10,88 @@ import UncapIndicator from '~components/UncapIndicator'
|
|||
import './index.scss'
|
||||
|
||||
interface Props {
|
||||
gridSummon: GridSummon
|
||||
children: React.ReactNode
|
||||
gridSummon: GridSummon
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const SummonHovercard = (props: Props) => {
|
||||
const router = useRouter()
|
||||
const { t } = useTranslation('common')
|
||||
const locale = (router.locale && ['en', 'ja'].includes(router.locale)) ? router.locale : 'en'
|
||||
const router = useRouter()
|
||||
const { t } = useTranslation('common')
|
||||
const locale =
|
||||
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
|
||||
|
||||
const Element = ['null', 'wind', 'fire', 'water', 'earth', 'dark', 'light']
|
||||
const Element = ['null', 'wind', 'fire', 'water', 'earth', 'dark', 'light']
|
||||
|
||||
const tintElement = Element[props.gridSummon.object.element]
|
||||
const wikiUrl = `https://gbf.wiki/${props.gridSummon.object.name.en.replaceAll(' ', '_')}`
|
||||
const tintElement = Element[props.gridSummon.object.element]
|
||||
const wikiUrl = `https://gbf.wiki/${props.gridSummon.object.name.en.replaceAll(
|
||||
' ',
|
||||
'_'
|
||||
)}`
|
||||
|
||||
function summonImage() {
|
||||
let imgSrc = ""
|
||||
function summonImage() {
|
||||
let imgSrc = ''
|
||||
|
||||
if (props.gridSummon) {
|
||||
const summon = props.gridSummon.object
|
||||
if (props.gridSummon) {
|
||||
const summon = props.gridSummon.object
|
||||
|
||||
const upgradedSummons = [
|
||||
'2040094000', '2040100000', '2040080000', '2040098000',
|
||||
'2040090000', '2040084000', '2040003000', '2040056000'
|
||||
]
|
||||
|
||||
let suffix = ''
|
||||
if (upgradedSummons.indexOf(summon.granblue_id.toString()) != -1 && props.gridSummon.uncap_level == 5)
|
||||
suffix = '_02'
|
||||
|
||||
// Generate the correct source for the summon
|
||||
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/summon-grid/${summon.granblue_id}${suffix}.jpg`
|
||||
}
|
||||
const upgradedSummons = [
|
||||
'2040094000',
|
||||
'2040100000',
|
||||
'2040080000',
|
||||
'2040098000',
|
||||
'2040090000',
|
||||
'2040084000',
|
||||
'2040003000',
|
||||
'2040056000',
|
||||
]
|
||||
|
||||
return imgSrc
|
||||
let suffix = ''
|
||||
if (
|
||||
upgradedSummons.indexOf(summon.granblue_id.toString()) != -1 &&
|
||||
props.gridSummon.uncap_level == 5
|
||||
)
|
||||
suffix = '_02'
|
||||
|
||||
// Generate the correct source for the summon
|
||||
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/summon-grid/${summon.granblue_id}${suffix}.jpg`
|
||||
}
|
||||
|
||||
return (
|
||||
<HoverCard.Root>
|
||||
<HoverCard.Trigger>
|
||||
{ props.children }
|
||||
</HoverCard.Trigger>
|
||||
<HoverCard.Content className="Weapon Hovercard">
|
||||
<div className="top">
|
||||
<div className="title">
|
||||
<h4>{ props.gridSummon.object.name[locale] }</h4>
|
||||
<img alt={props.gridSummon.object.name[locale]} src={summonImage()} />
|
||||
</div>
|
||||
<div className="subInfo">
|
||||
<div className="icons">
|
||||
<WeaponLabelIcon labelType={Element[props.gridSummon.object.element]}/>
|
||||
</div>
|
||||
<UncapIndicator
|
||||
type="summon"
|
||||
ulb={props.gridSummon.object.uncap.ulb || false}
|
||||
flb={props.gridSummon.object.uncap.flb || false}
|
||||
special={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<a className={`Button ${tintElement}`} href={wikiUrl} target="_new">{t('buttons.wiki')}</a>
|
||||
<HoverCard.Arrow />
|
||||
</HoverCard.Content>
|
||||
</HoverCard.Root>
|
||||
)
|
||||
return imgSrc
|
||||
}
|
||||
|
||||
return (
|
||||
<HoverCard.Root>
|
||||
<HoverCard.Trigger>{props.children}</HoverCard.Trigger>
|
||||
<HoverCard.Content className="Weapon Hovercard">
|
||||
<div className="top">
|
||||
<div className="title">
|
||||
<h4>{props.gridSummon.object.name[locale]}</h4>
|
||||
<img
|
||||
alt={props.gridSummon.object.name[locale]}
|
||||
src={summonImage()}
|
||||
/>
|
||||
</div>
|
||||
<div className="subInfo">
|
||||
<div className="icons">
|
||||
<WeaponLabelIcon
|
||||
labelType={Element[props.gridSummon.object.element]}
|
||||
/>
|
||||
</div>
|
||||
<UncapIndicator
|
||||
type="summon"
|
||||
ulb={props.gridSummon.object.uncap.ulb || false}
|
||||
flb={props.gridSummon.object.uncap.flb || false}
|
||||
special={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<a className={`Button ${tintElement}`} href={wikiUrl} target="_new">
|
||||
{t('buttons.wiki')}
|
||||
</a>
|
||||
<HoverCard.Arrow />
|
||||
</HoverCard.Content>
|
||||
</HoverCard.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export default SummonHovercard
|
||||
|
||||
|
|
|
|||
|
|
@ -1,63 +1,67 @@
|
|||
.SummonResult {
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
gap: $unit;
|
||||
padding: $unit * 1.5;
|
||||
|
||||
&:hover {
|
||||
background: var(--button-contained-bg);
|
||||
cursor: pointer;
|
||||
|
||||
.Info h5 {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
background: $grey-80;
|
||||
border-radius: 6px;
|
||||
display: inline-block;
|
||||
height: auto;
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
.Info {
|
||||
display: flex;
|
||||
gap: $unit;
|
||||
padding: $unit * 1.5;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
gap: $unit-half;
|
||||
|
||||
&:hover {
|
||||
background: $grey-90;
|
||||
cursor: pointer;
|
||||
h5 {
|
||||
color: var(--text-secondary);
|
||||
display: inline-block;
|
||||
font-size: $font-medium;
|
||||
font-weight: $medium;
|
||||
}
|
||||
|
||||
img {
|
||||
background: $grey-80;
|
||||
border-radius: 6px;
|
||||
display: inline-block;
|
||||
height: auto;
|
||||
width: 120px;
|
||||
.UncapIndicator {
|
||||
justify-content: left;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.Info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
gap: calc($unit / 2);
|
||||
.stars {
|
||||
display: inline-block;
|
||||
color: #ffa15e;
|
||||
font-size: $font-xlarge;
|
||||
|
||||
h5 {
|
||||
color: #555;
|
||||
display: inline-block;
|
||||
font-size: $font-medium;
|
||||
font-weight: $medium;
|
||||
}
|
||||
|
||||
.UncapIndicator {
|
||||
justify-content: left;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.stars {
|
||||
display: inline-block;
|
||||
color: #FFA15E;
|
||||
font-size: $font-xlarge;
|
||||
|
||||
& > span {
|
||||
color: #65DAFF;
|
||||
}
|
||||
}
|
||||
|
||||
.tags {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: calc($unit / 2);
|
||||
|
||||
.WeaponLabelIcon {
|
||||
$aspect-ratio: calc(25 / 60);
|
||||
$height: 22px;
|
||||
background-size: calc($height / $aspect-ratio) $height;
|
||||
background-repeat: no-repeat;
|
||||
height: $height;
|
||||
width: calc($height / $aspect-ratio);
|
||||
}
|
||||
}
|
||||
& > span {
|
||||
color: #65daff;
|
||||
}
|
||||
}
|
||||
|
||||
.tags {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: calc($unit / 2);
|
||||
|
||||
.WeaponLabelIcon {
|
||||
$aspect-ratio: calc(25 / 60);
|
||||
$height: 22px;
|
||||
background-size: calc($height / $aspect-ratio) $height;
|
||||
background-repeat: no-repeat;
|
||||
height: $height;
|
||||
width: calc($height / $aspect-ratio);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,35 +7,39 @@ import WeaponLabelIcon from '~components/WeaponLabelIcon'
|
|||
import './index.scss'
|
||||
|
||||
interface Props {
|
||||
data: Summon
|
||||
onClick: () => void
|
||||
data: Summon
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
const Element = ['null', 'wind', 'fire', 'water', 'earth', 'dark', 'light']
|
||||
|
||||
const SummonResult = (props: Props) => {
|
||||
const router = useRouter()
|
||||
const locale = (router.locale && ['en', 'ja'].includes(router.locale)) ? router.locale : 'en'
|
||||
const router = useRouter()
|
||||
const locale =
|
||||
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
|
||||
|
||||
const summon = props.data
|
||||
|
||||
return (
|
||||
<li className="SummonResult" onClick={props.onClick}>
|
||||
<img alt={summon.name[locale]} src={`${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/summon-grid/${summon.granblue_id}.jpg`} />
|
||||
<div className="Info">
|
||||
<h5>{summon.name[locale]}</h5>
|
||||
<UncapIndicator
|
||||
type="summon"
|
||||
flb={summon.uncap.flb}
|
||||
ulb={summon.uncap.ulb}
|
||||
special={false}
|
||||
/>
|
||||
<div className="tags">
|
||||
<WeaponLabelIcon labelType={Element[summon.element]} />
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
const summon = props.data
|
||||
|
||||
return (
|
||||
<li className="SummonResult" onClick={props.onClick}>
|
||||
<img
|
||||
alt={summon.name[locale]}
|
||||
src={`${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/summon-grid/${summon.granblue_id}.jpg`}
|
||||
/>
|
||||
<div className="Info">
|
||||
<h5>{summon.name[locale]}</h5>
|
||||
<UncapIndicator
|
||||
type="summon"
|
||||
flb={summon.uncap.flb}
|
||||
ulb={summon.uncap.ulb}
|
||||
special={false}
|
||||
/>
|
||||
<div className="tags">
|
||||
<WeaponLabelIcon labelType={Element[summon.element]} />
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
export default SummonResult
|
||||
export default SummonResult
|
||||
|
|
|
|||
|
|
@ -13,93 +13,122 @@ import { emptyElementState, emptyRarityState } from '~utils/emptyStates'
|
|||
import { elements, rarities } from '~utils/stateValues'
|
||||
|
||||
interface Props {
|
||||
sendFilters: (filters: { [key: string]: number[] }) => void
|
||||
sendFilters: (filters: { [key: string]: number[] }) => void
|
||||
}
|
||||
|
||||
const SummonSearchFilterBar = (props: Props) => {
|
||||
const { t } = useTranslation('common')
|
||||
const { t } = useTranslation('common')
|
||||
|
||||
const [rarityMenu, setRarityMenu] = useState(false)
|
||||
const [elementMenu, setElementMenu] = useState(false)
|
||||
const [rarityMenu, setRarityMenu] = useState(false)
|
||||
const [elementMenu, setElementMenu] = useState(false)
|
||||
|
||||
const [rarityState, setRarityState] = useState<RarityState>(emptyRarityState)
|
||||
const [elementState, setElementState] = useState<ElementState>(emptyElementState)
|
||||
const [rarityState, setRarityState] = useState<RarityState>(emptyRarityState)
|
||||
const [elementState, setElementState] =
|
||||
useState<ElementState>(emptyElementState)
|
||||
|
||||
function rarityMenuOpened(open: boolean) {
|
||||
if (open) {
|
||||
setRarityMenu(true)
|
||||
setElementMenu(false)
|
||||
} else setRarityMenu(false)
|
||||
function rarityMenuOpened(open: boolean) {
|
||||
if (open) {
|
||||
setRarityMenu(true)
|
||||
setElementMenu(false)
|
||||
} else setRarityMenu(false)
|
||||
}
|
||||
|
||||
function elementMenuOpened(open: boolean) {
|
||||
if (open) {
|
||||
setRarityMenu(false)
|
||||
setElementMenu(true)
|
||||
} else setElementMenu(false)
|
||||
}
|
||||
|
||||
function handleRarityChange(checked: boolean, key: string) {
|
||||
let newRarityState = cloneDeep(rarityState)
|
||||
newRarityState[key].checked = checked
|
||||
setRarityState(newRarityState)
|
||||
}
|
||||
|
||||
function handleElementChange(checked: boolean, key: string) {
|
||||
let newElementState = cloneDeep(elementState)
|
||||
newElementState[key].checked = checked
|
||||
setElementState(newElementState)
|
||||
}
|
||||
|
||||
function sendFilters() {
|
||||
const checkedRarityFilters = Object.values(rarityState)
|
||||
.filter((x) => x.checked)
|
||||
.map((x, i) => x.id)
|
||||
const checkedElementFilters = Object.values(elementState)
|
||||
.filter((x) => x.checked)
|
||||
.map((x, i) => x.id)
|
||||
|
||||
const filters = {
|
||||
rarity: checkedRarityFilters,
|
||||
element: checkedElementFilters,
|
||||
}
|
||||
|
||||
function elementMenuOpened(open: boolean) {
|
||||
if (open) {
|
||||
setRarityMenu(false)
|
||||
setElementMenu(true)
|
||||
} else setElementMenu(false)
|
||||
}
|
||||
props.sendFilters(filters)
|
||||
}
|
||||
|
||||
function handleRarityChange(checked: boolean, key: string) {
|
||||
let newRarityState = cloneDeep(rarityState)
|
||||
newRarityState[key].checked = checked
|
||||
setRarityState(newRarityState)
|
||||
}
|
||||
useEffect(() => {
|
||||
sendFilters()
|
||||
}, [rarityState, elementState])
|
||||
|
||||
function handleElementChange(checked: boolean, key: string) {
|
||||
let newElementState = cloneDeep(elementState)
|
||||
newElementState[key].checked = checked
|
||||
setElementState(newElementState)
|
||||
}
|
||||
|
||||
function sendFilters() {
|
||||
const checkedRarityFilters = Object.values(rarityState).filter(x => x.checked).map((x, i) => x.id)
|
||||
const checkedElementFilters = Object.values(elementState).filter(x => x.checked).map((x, i) => x.id)
|
||||
|
||||
const filters = {
|
||||
rarity: checkedRarityFilters,
|
||||
element: checkedElementFilters
|
||||
return (
|
||||
<div className="SearchFilterBar">
|
||||
<SearchFilter
|
||||
label={t('filters.labels.rarity')}
|
||||
numSelected={
|
||||
Object.values(rarityState)
|
||||
.map((x) => x.checked)
|
||||
.filter(Boolean).length
|
||||
}
|
||||
open={rarityMenu}
|
||||
onOpenChange={rarityMenuOpened}
|
||||
>
|
||||
<DropdownMenu.Label className="Label">
|
||||
{t('filters.labels.rarity')}
|
||||
</DropdownMenu.Label>
|
||||
{Array.from(Array(rarities.length)).map((x, i) => {
|
||||
return (
|
||||
<SearchFilterCheckboxItem
|
||||
key={rarities[i]}
|
||||
onCheckedChange={handleRarityChange}
|
||||
checked={rarityState[rarities[i]].checked}
|
||||
valueKey={rarities[i]}
|
||||
>
|
||||
{t(`rarities.${rarities[i]}`)}
|
||||
</SearchFilterCheckboxItem>
|
||||
)
|
||||
})}
|
||||
</SearchFilter>
|
||||
|
||||
props.sendFilters(filters)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
sendFilters()
|
||||
}, [rarityState, elementState])
|
||||
|
||||
return (
|
||||
<div className="SearchFilterBar">
|
||||
<SearchFilter label={t('filters.labels.rarity')} numSelected={Object.values(rarityState).map(x => x.checked).filter(Boolean).length} open={rarityMenu} onOpenChange={rarityMenuOpened}>
|
||||
<DropdownMenu.Label className="Label">{t('filters.labels.rarity')}</DropdownMenu.Label>
|
||||
{ Array.from(Array(rarities.length)).map((x, i) => {
|
||||
return (
|
||||
<SearchFilterCheckboxItem
|
||||
key={rarities[i]}
|
||||
onCheckedChange={handleRarityChange}
|
||||
checked={rarityState[rarities[i]].checked}
|
||||
valueKey={rarities[i]}>
|
||||
{t(`rarities.${rarities[i]}`)}
|
||||
</SearchFilterCheckboxItem>
|
||||
)}
|
||||
) }
|
||||
</SearchFilter>
|
||||
|
||||
<SearchFilter label={t('filters.labels.element')} numSelected={Object.values(elementState).map(x => x.checked).filter(Boolean).length} open={elementMenu} onOpenChange={elementMenuOpened}>
|
||||
<DropdownMenu.Label className="Label">{t('filters.labels.element')}</DropdownMenu.Label>
|
||||
{ Array.from(Array(elements.length)).map((x, i) => {
|
||||
return (
|
||||
<SearchFilterCheckboxItem
|
||||
key={elements[i]}
|
||||
onCheckedChange={handleElementChange}
|
||||
checked={elementState[elements[i]].checked}
|
||||
valueKey={elements[i]}>
|
||||
{t(`elements.${elements[i]}`)}
|
||||
</SearchFilterCheckboxItem>
|
||||
)}
|
||||
) }
|
||||
</SearchFilter>
|
||||
</div>
|
||||
)
|
||||
<SearchFilter
|
||||
label={t('filters.labels.element')}
|
||||
numSelected={
|
||||
Object.values(elementState)
|
||||
.map((x) => x.checked)
|
||||
.filter(Boolean).length
|
||||
}
|
||||
open={elementMenu}
|
||||
onOpenChange={elementMenuOpened}
|
||||
>
|
||||
<DropdownMenu.Label className="Label">
|
||||
{t('filters.labels.element')}
|
||||
</DropdownMenu.Label>
|
||||
{Array.from(Array(elements.length)).map((x, i) => {
|
||||
return (
|
||||
<SearchFilterCheckboxItem
|
||||
key={elements[i]}
|
||||
onCheckedChange={handleElementChange}
|
||||
checked={elementState[elements[i]].checked}
|
||||
valueKey={elements[i]}
|
||||
>
|
||||
{t(`elements.${elements[i]}`)}
|
||||
</SearchFilterCheckboxItem>
|
||||
)
|
||||
})}
|
||||
</SearchFilter>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SummonSearchFilterBar
|
||||
|
|
|
|||
|
|
@ -1,106 +1,107 @@
|
|||
.SummonUnit {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
&.main .SummonImage,
|
||||
&.friend .SummonImage {
|
||||
aspect-ratio: 182 / 315;
|
||||
width: 182px;
|
||||
height: auto;
|
||||
&.main .SummonImage,
|
||||
&.friend .SummonImage {
|
||||
aspect-ratio: 182 / 315;
|
||||
width: 182px;
|
||||
height: auto;
|
||||
|
||||
@media (max-width: $medium-screen) {
|
||||
width: 20.3vw;
|
||||
}
|
||||
@media (max-width: $medium-screen) {
|
||||
width: 20.3vw;
|
||||
}
|
||||
}
|
||||
|
||||
&.grid {
|
||||
// max-width: 148px;
|
||||
// min-height: 141px;
|
||||
min-height: 180px;
|
||||
&.grid {
|
||||
// max-width: 148px;
|
||||
// min-height: 141px;
|
||||
min-height: 180px;
|
||||
|
||||
@media (max-width: $medium-screen) {
|
||||
min-height: 16.5vw;
|
||||
}
|
||||
|
||||
.SummonImage {
|
||||
aspect-ratio: 148 / 111;
|
||||
list-style-type: none;
|
||||
width: 148px;
|
||||
height: auto;
|
||||
|
||||
@media (max-width: $medium-screen) {
|
||||
width: 20vw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.friend {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
&.main.editable .SummonImage:hover,
|
||||
&.friend.editable .SummonImage:hover {
|
||||
transform: $scale-tall;
|
||||
}
|
||||
|
||||
&.editable .SummonImage:hover {
|
||||
border: $hover-stroke;
|
||||
box-shadow: $hover-shadow;
|
||||
cursor: pointer;
|
||||
transform: $scale-wide;
|
||||
@media (max-width: $medium-screen) {
|
||||
min-height: 16.5vw;
|
||||
}
|
||||
|
||||
.SummonImage {
|
||||
background: white;
|
||||
border: 1px solid rgba(0, 0, 0, 0);
|
||||
border-radius: $unit;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
transition: all 0.18s ease-in-out;
|
||||
aspect-ratio: 148 / 111;
|
||||
list-style-type: none;
|
||||
width: 148px;
|
||||
height: auto;
|
||||
|
||||
&:hover .icon svg {
|
||||
fill: $grey-40;
|
||||
}
|
||||
@media (max-width: $medium-screen) {
|
||||
width: 20vw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
position: absolute;
|
||||
height: $unit * 3;
|
||||
width: $unit * 3;
|
||||
z-index: 1;
|
||||
|
||||
svg {
|
||||
fill: $grey-70;
|
||||
}
|
||||
}
|
||||
&.friend {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
&.main.editable .SummonImage:hover,
|
||||
&.friend.editable .SummonImage:hover {
|
||||
transform: $scale-tall;
|
||||
}
|
||||
|
||||
&.editable .SummonImage:hover {
|
||||
border: $hover-stroke;
|
||||
box-shadow: $hover-shadow;
|
||||
cursor: pointer;
|
||||
transform: $scale-wide;
|
||||
}
|
||||
|
||||
.SummonImage {
|
||||
background: var(--card-bg);
|
||||
border: 1px solid rgba(0, 0, 0, 0);
|
||||
border-radius: $unit;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
transition: all 0.18s ease-in-out;
|
||||
|
||||
&:hover .icon svg {
|
||||
fill: var(--icon-secondary-hover);
|
||||
}
|
||||
|
||||
&.filled h3 {
|
||||
display: block;
|
||||
}
|
||||
.icon {
|
||||
position: absolute;
|
||||
height: $unit * 3;
|
||||
width: $unit * 3;
|
||||
z-index: 1;
|
||||
|
||||
&.filled ul {
|
||||
display: flex;
|
||||
svg {
|
||||
fill: var(--icon-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h3, ul {
|
||||
display: none;
|
||||
}
|
||||
&.filled h3 {
|
||||
display: block;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: #333;
|
||||
font-size: $font-regular;
|
||||
font-weight: $normal;
|
||||
line-height: 1.1;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
&.filled ul {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
img {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
z-index: 2;
|
||||
}
|
||||
h3,
|
||||
ul {
|
||||
display: none;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: var(--text-primary);
|
||||
font-size: $font-regular;
|
||||
font-weight: $normal;
|
||||
line-height: 1.1;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
img {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
import React, { useEffect, useState } from "react"
|
||||
import { useRouter } from "next/router"
|
||||
import { useTranslation } from "next-i18next"
|
||||
import classnames from "classnames"
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import classnames from 'classnames'
|
||||
|
||||
import SearchModal from "~components/SearchModal"
|
||||
import SummonHovercard from "~components/SummonHovercard"
|
||||
import UncapIndicator from "~components/UncapIndicator"
|
||||
import PlusIcon from "~public/icons/Add.svg"
|
||||
import SearchModal from '~components/SearchModal'
|
||||
import SummonHovercard from '~components/SummonHovercard'
|
||||
import UncapIndicator from '~components/UncapIndicator'
|
||||
import PlusIcon from '~public/icons/Add.svg'
|
||||
|
||||
import type { SearchableObject } from "~types"
|
||||
import type { SearchableObject } from '~types'
|
||||
|
||||
import "./index.scss"
|
||||
import './index.scss'
|
||||
|
||||
interface Props {
|
||||
gridSummon: GridSummon | undefined
|
||||
|
|
@ -22,13 +22,13 @@ interface Props {
|
|||
}
|
||||
|
||||
const SummonUnit = (props: Props) => {
|
||||
const { t } = useTranslation("common")
|
||||
const { t } = useTranslation('common')
|
||||
|
||||
const [imageUrl, setImageUrl] = useState("")
|
||||
const [imageUrl, setImageUrl] = useState('')
|
||||
|
||||
const router = useRouter()
|
||||
const locale =
|
||||
router.locale && ["en", "ja"].includes(router.locale) ? router.locale : "en"
|
||||
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
|
||||
|
||||
const classes = classnames({
|
||||
SummonUnit: true,
|
||||
|
|
@ -47,33 +47,33 @@ const SummonUnit = (props: Props) => {
|
|||
})
|
||||
|
||||
function generateImageUrl() {
|
||||
let imgSrc = ""
|
||||
let imgSrc = ''
|
||||
if (props.gridSummon) {
|
||||
const summon = props.gridSummon.object!
|
||||
|
||||
const upgradedSummons = [
|
||||
"2040094000",
|
||||
"2040100000",
|
||||
"2040080000",
|
||||
"2040098000",
|
||||
"2040090000",
|
||||
"2040084000",
|
||||
"2040003000",
|
||||
"2040056000",
|
||||
"2040020000",
|
||||
"2040034000",
|
||||
"2040028000",
|
||||
"2040027000",
|
||||
"2040046000",
|
||||
"2040047000",
|
||||
'2040094000',
|
||||
'2040100000',
|
||||
'2040080000',
|
||||
'2040098000',
|
||||
'2040090000',
|
||||
'2040084000',
|
||||
'2040003000',
|
||||
'2040056000',
|
||||
'2040020000',
|
||||
'2040034000',
|
||||
'2040028000',
|
||||
'2040027000',
|
||||
'2040046000',
|
||||
'2040047000',
|
||||
]
|
||||
|
||||
let suffix = ""
|
||||
let suffix = ''
|
||||
if (
|
||||
upgradedSummons.indexOf(summon.granblue_id.toString()) != -1 &&
|
||||
props.gridSummon.uncap_level == 5
|
||||
)
|
||||
suffix = "_02"
|
||||
suffix = '_02'
|
||||
|
||||
// Generate the correct source for the summon
|
||||
if (props.unitType == 0 || props.unitType == 2)
|
||||
|
|
@ -98,14 +98,14 @@ const SummonUnit = (props: Props) => {
|
|||
<PlusIcon />
|
||||
</span>
|
||||
) : (
|
||||
""
|
||||
''
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
const editableImage = (
|
||||
<SearchModal
|
||||
placeholderText={t("search.placeholders.summon")}
|
||||
placeholderText={t('search.placeholders.summon')}
|
||||
fromPosition={props.position}
|
||||
object="summons"
|
||||
send={props.updateObject}
|
||||
|
|
@ -127,7 +127,7 @@ const SummonUnit = (props: Props) => {
|
|||
special={false}
|
||||
/>
|
||||
) : (
|
||||
""
|
||||
''
|
||||
)}
|
||||
<h3 className="SummonName">{summon?.name[locale]}</h3>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,18 @@
|
|||
.Fieldset textarea {
|
||||
color: $grey-00;
|
||||
font-family: system-ui, -apple-system, 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
line-height: 21px;
|
||||
}
|
||||
$offset: 2px;
|
||||
|
||||
background-color: var(--input-bg);
|
||||
border: $offset solid transparent;
|
||||
border-radius: $input-corner;
|
||||
box-sizing: border-box;
|
||||
color: var(--text-primary);
|
||||
display: block;
|
||||
line-height: 21px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
padding: $unit-2x calc($unit-2x - $offset);
|
||||
|
||||
&:focus-within {
|
||||
border: $offset solid $blue;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,32 +2,31 @@ import React from 'react'
|
|||
import './index.scss'
|
||||
|
||||
interface Props {
|
||||
fieldName: string
|
||||
placeholder: string
|
||||
value?: string
|
||||
error: string
|
||||
onBlur?: (event: React.ChangeEvent<HTMLTextAreaElement>) => void
|
||||
onChange?: (event: React.ChangeEvent<HTMLTextAreaElement>) => void
|
||||
fieldName: string
|
||||
placeholder: string
|
||||
value?: string
|
||||
error: string
|
||||
onBlur?: (event: React.ChangeEvent<HTMLTextAreaElement>) => void
|
||||
onChange?: (event: React.ChangeEvent<HTMLTextAreaElement>) => void
|
||||
}
|
||||
|
||||
const TextFieldset = React.forwardRef<HTMLTextAreaElement, Props>(function fieldSet(props, ref) {
|
||||
const TextFieldset = React.forwardRef<HTMLTextAreaElement, Props>(
|
||||
function fieldSet(props, ref) {
|
||||
return (
|
||||
<fieldset className="Fieldset">
|
||||
<textarea
|
||||
className="Input"
|
||||
name={props.fieldName}
|
||||
placeholder={props.placeholder}
|
||||
defaultValue={props.value || ''}
|
||||
onBlur={props.onBlur}
|
||||
onChange={props.onChange}
|
||||
ref={ref}
|
||||
/>
|
||||
{
|
||||
props.error.length > 0 &&
|
||||
<p className='InputError'>{props.error}</p>
|
||||
}
|
||||
</fieldset>
|
||||
<fieldset className="Fieldset">
|
||||
<textarea
|
||||
className="Input"
|
||||
name={props.fieldName}
|
||||
placeholder={props.placeholder}
|
||||
defaultValue={props.value || ''}
|
||||
onBlur={props.onBlur}
|
||||
onChange={props.onChange}
|
||||
ref={ref}
|
||||
/>
|
||||
{props.error.length > 0 && <p className="InputError">{props.error}</p>}
|
||||
</fieldset>
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
export default TextFieldset
|
||||
export default TextFieldset
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
.toggle-switch {
|
||||
background: #fff;
|
||||
background: var(--card-bg);
|
||||
border-radius: 18px;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
|
|
@ -30,7 +30,7 @@
|
|||
}
|
||||
|
||||
&-switch {
|
||||
background: #e4e4e4;
|
||||
background: var(--switch-nub); // #e4e4e4;
|
||||
display: block;
|
||||
width: 24px;
|
||||
margin: 5px;
|
||||
|
|
@ -40,14 +40,18 @@
|
|||
right: 24px;
|
||||
border-radius: 17px;
|
||||
transition: all 0.18s ease-in 0s;
|
||||
|
||||
&:hover {
|
||||
background: var(--background);
|
||||
}
|
||||
}
|
||||
|
||||
&-checkbox:checked + &-label {
|
||||
background: #ECEBFF;
|
||||
background: var(--extra-purple-bg);
|
||||
}
|
||||
|
||||
&-checkbox:checked + &-label &-switch {
|
||||
background: #8C86FF;
|
||||
background: var(--extra-purple-primary);
|
||||
}
|
||||
|
||||
&-checkbox:checked + &-label {
|
||||
|
|
@ -58,4 +62,4 @@
|
|||
right: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,29 +3,29 @@ import React from 'react'
|
|||
import './index.scss'
|
||||
|
||||
interface Props {
|
||||
name: string
|
||||
checked: boolean
|
||||
editable: boolean
|
||||
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void
|
||||
name: string
|
||||
checked: boolean
|
||||
editable: boolean
|
||||
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void
|
||||
}
|
||||
|
||||
const ToggleSwitch: React.FC<Props> = (props: Props) => {
|
||||
return (
|
||||
<div className="toggle-switch">
|
||||
<input
|
||||
className="toggle-switch-checkbox"
|
||||
name={props.name}
|
||||
id={props.name}
|
||||
type="checkbox"
|
||||
checked={props.checked}
|
||||
disabled={!props.editable}
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
<label className="toggle-switch-label" htmlFor={props.name}>
|
||||
<span className="toggle-switch-switch" />
|
||||
</label>
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<div className="toggle-switch">
|
||||
<input
|
||||
className="toggle-switch-checkbox"
|
||||
name={props.name}
|
||||
id={props.name}
|
||||
type="checkbox"
|
||||
checked={props.checked}
|
||||
disabled={!props.editable}
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
<label className="toggle-switch-label" htmlFor={props.name}>
|
||||
<span className="toggle-switch-switch" />
|
||||
</label>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ToggleSwitch
|
||||
export default ToggleSwitch
|
||||
|
|
|
|||
|
|
@ -1,25 +1,30 @@
|
|||
import React from "react"
|
||||
import { useSnapshot } from "valtio"
|
||||
import { getCookie, deleteCookie } from "cookies-next"
|
||||
import { useRouter } from "next/router"
|
||||
import { useTranslation } from "next-i18next"
|
||||
import React from 'react'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import { getCookie, deleteCookie } from 'cookies-next'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
|
||||
import clonedeep from "lodash.clonedeep"
|
||||
import clonedeep from 'lodash.clonedeep'
|
||||
|
||||
import api from "~utils/api"
|
||||
import { accountState, initialAccountState } from "~utils/accountState"
|
||||
import { appState, initialAppState } from "~utils/appState"
|
||||
import api from '~utils/api'
|
||||
import { accountState, initialAccountState } from '~utils/accountState'
|
||||
import { appState, initialAppState } from '~utils/appState'
|
||||
|
||||
import Header from "~components/Header"
|
||||
import Button from "~components/Button"
|
||||
import HeaderMenu from "~components/HeaderMenu"
|
||||
import Header from '~components/Header'
|
||||
import Button from '~components/Button'
|
||||
import HeaderMenu from '~components/HeaderMenu'
|
||||
|
||||
import AddIcon from '~public/icons/Add.svg'
|
||||
import LinkIcon from '~public/icons/Link.svg'
|
||||
import MenuIcon from '~public/icons/Menu.svg'
|
||||
import SaveIcon from '~public/icons/Save.svg'
|
||||
|
||||
const TopHeader = () => {
|
||||
const { t } = useTranslation("common")
|
||||
const { t } = useTranslation('common')
|
||||
|
||||
// Cookies
|
||||
const accountCookie = getCookie("account")
|
||||
const userCookie = getCookie("user")
|
||||
const accountCookie = getCookie('account')
|
||||
const userCookie = getCookie('user')
|
||||
|
||||
const headers = {}
|
||||
// accountCookies.account != null
|
||||
|
|
@ -33,19 +38,19 @@ const TopHeader = () => {
|
|||
const router = useRouter()
|
||||
|
||||
function copyToClipboard() {
|
||||
const el = document.createElement("input")
|
||||
const el = document.createElement('input')
|
||||
el.value = window.location.href
|
||||
el.id = "url-input"
|
||||
el.id = 'url-input'
|
||||
document.body.appendChild(el)
|
||||
|
||||
el.select()
|
||||
document.execCommand("copy")
|
||||
document.execCommand('copy')
|
||||
el.remove()
|
||||
}
|
||||
|
||||
function newParty() {
|
||||
// Push the root URL
|
||||
router.push("/")
|
||||
router.push('/')
|
||||
|
||||
// Clean state
|
||||
const resetState = clonedeep(initialAppState)
|
||||
|
|
@ -58,18 +63,18 @@ const TopHeader = () => {
|
|||
}
|
||||
|
||||
function logout() {
|
||||
deleteCookie("account")
|
||||
deleteCookie("user")
|
||||
deleteCookie('account')
|
||||
deleteCookie('user')
|
||||
|
||||
// Clean state
|
||||
const resetState = clonedeep(initialAccountState)
|
||||
Object.keys(resetState).forEach((key) => {
|
||||
if (key !== "language") accountState[key] = resetState[key]
|
||||
if (key !== 'language') accountState[key] = resetState[key]
|
||||
})
|
||||
|
||||
if (router.route != "/new") appState.party.editable = false
|
||||
if (router.route != '/new') appState.party.editable = false
|
||||
|
||||
router.push("/")
|
||||
router.push('/')
|
||||
return false
|
||||
}
|
||||
|
||||
|
|
@ -83,7 +88,7 @@ const TopHeader = () => {
|
|||
api.saveTeam({ id: party.id, params: headers }).then((response) => {
|
||||
if (response.status == 201) appState.party.favorited = true
|
||||
})
|
||||
else console.error("Failed to save team: No party ID")
|
||||
else console.error('Failed to save team: No party ID')
|
||||
}
|
||||
|
||||
function unsaveFavorite() {
|
||||
|
|
@ -91,13 +96,29 @@ const TopHeader = () => {
|
|||
api.unsaveTeam({ id: party.id, params: headers }).then((response) => {
|
||||
if (response.status == 200) appState.party.favorited = false
|
||||
})
|
||||
else console.error("Failed to unsave team: No party ID")
|
||||
else console.error('Failed to unsave team: No party ID')
|
||||
}
|
||||
|
||||
const copyButton = () => {
|
||||
if (router.route === '/p/[party]')
|
||||
return (
|
||||
<Button
|
||||
accessoryIcon={<LinkIcon className="stroke" />}
|
||||
blended={true}
|
||||
text={t('buttons.copy')}
|
||||
onClick={copyToClipboard}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const leftNav = () => {
|
||||
return (
|
||||
<div className="dropdown">
|
||||
<Button icon="menu">{t("buttons.menu")}</Button>
|
||||
<Button
|
||||
accessoryIcon={<MenuIcon />}
|
||||
blended={true}
|
||||
text={t('buttons.menu')}
|
||||
/>
|
||||
{account.user ? (
|
||||
<HeaderMenu
|
||||
authenticated={account.authorized}
|
||||
|
|
@ -114,36 +135,41 @@ const TopHeader = () => {
|
|||
const saveButton = () => {
|
||||
if (party.favorited)
|
||||
return (
|
||||
<Button icon="save" active={true} onClick={toggleFavorite}>
|
||||
Saved
|
||||
</Button>
|
||||
<Button
|
||||
accessoryIcon={<SaveIcon />}
|
||||
blended={true}
|
||||
text="Saved"
|
||||
onClick={toggleFavorite}
|
||||
/>
|
||||
)
|
||||
else
|
||||
return (
|
||||
<Button icon="save" onClick={toggleFavorite}>
|
||||
Save
|
||||
</Button>
|
||||
<Button
|
||||
accessoryIcon={<SaveIcon />}
|
||||
blended={true}
|
||||
text="Save"
|
||||
onClick={toggleFavorite}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const rightNav = () => {
|
||||
return (
|
||||
<div>
|
||||
{router.route === "/p/[party]" &&
|
||||
{router.route === '/p/[party]' &&
|
||||
account.user &&
|
||||
(!party.user || party.user.id !== account.user.id)
|
||||
? saveButton()
|
||||
: ""}
|
||||
{router.route === "/p/[party]" ? (
|
||||
<Button icon="link" onClick={copyToClipboard}>
|
||||
{t("buttons.copy")}
|
||||
</Button>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
<Button icon="new" onClick={newParty}>
|
||||
{t("buttons.new")}
|
||||
</Button>
|
||||
: ''}
|
||||
|
||||
{copyButton()}
|
||||
|
||||
<Button
|
||||
accessoryIcon={<AddIcon className="Add" />}
|
||||
blended={true}
|
||||
text={t('buttons.new')}
|
||||
onClick={newParty}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
.UncapIndicator {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-content: center;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-content: center;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import React, { useEffect, useRef, useState } from "react"
|
||||
import UncapStar from "~components/UncapStar"
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import UncapStar from '~components/UncapStar'
|
||||
|
||||
import "./index.scss"
|
||||
import './index.scss'
|
||||
|
||||
interface Props {
|
||||
type: "character" | "weapon" | "summon"
|
||||
type: 'character' | 'weapon' | 'summon'
|
||||
rarity?: number
|
||||
uncapLevel?: number
|
||||
flb: boolean
|
||||
|
|
@ -20,7 +20,7 @@ const UncapIndicator = (props: Props) => {
|
|||
function setNumStars() {
|
||||
let numStars
|
||||
|
||||
if (props.type === "character") {
|
||||
if (props.type === 'character') {
|
||||
if (props.special) {
|
||||
if (props.ulb) {
|
||||
numStars = 5
|
||||
|
|
@ -109,13 +109,13 @@ const UncapIndicator = (props: Props) => {
|
|||
return (
|
||||
<ul className="UncapIndicator">
|
||||
{Array.from(Array(numStars)).map((x, i) => {
|
||||
if (props.type === "character" && i > 4) {
|
||||
if (props.type === 'character' && i > 4) {
|
||||
if (props.special) return ulb(i)
|
||||
else return transcendence(i)
|
||||
} else if (
|
||||
(props.special && props.type === "character" && i == 3) ||
|
||||
(props.type === "character" && i == 4) ||
|
||||
(props.type !== "character" && i > 2)
|
||||
(props.special && props.type === 'character' && i == 3) ||
|
||||
(props.type === 'character' && i == 4) ||
|
||||
(props.type !== 'character' && i > 2)
|
||||
) {
|
||||
return flb(i)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -1,55 +1,55 @@
|
|||
.UncapStar {
|
||||
background-repeat: no-repeat;
|
||||
background-size: 18px 18px;
|
||||
display: block;
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 18px 18px;
|
||||
display: block;
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
&.empty,
|
||||
&.empty.mlb,
|
||||
&.empty.flb,
|
||||
&.empty.ulb,
|
||||
&.empty.special {
|
||||
background: url('/icons/uncap/empty.svg');
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.2);
|
||||
background: url('/icons/uncap/empty-hover.svg');
|
||||
}
|
||||
}
|
||||
|
||||
&.empty,
|
||||
&.empty.mlb,
|
||||
&.empty.flb,
|
||||
&.empty.ulb,
|
||||
&.empty.special {
|
||||
background: url('/icons/uncap/empty.svg');
|
||||
&.mlb {
|
||||
background: url('/icons/uncap/yellow.svg');
|
||||
|
||||
&:hover {
|
||||
background: url('/icons/uncap/empty-hover.svg');
|
||||
}
|
||||
&:hover {
|
||||
background: url('/icons/uncap/yellow-hover.svg');
|
||||
}
|
||||
}
|
||||
|
||||
&.mlb {
|
||||
background: url('/icons/uncap/yellow.svg');
|
||||
&.special {
|
||||
background: url('/icons/uncap/red.svg');
|
||||
|
||||
&:hover {
|
||||
background: url('/icons/uncap/yellow-hover.svg');
|
||||
}
|
||||
&:hover {
|
||||
background: url('/icons/uncap/red-hover.svg');
|
||||
}
|
||||
}
|
||||
|
||||
&.special {
|
||||
background: url('/icons/uncap/red.svg');
|
||||
&.flb {
|
||||
background: url('/icons/uncap/blue.svg');
|
||||
|
||||
&:hover {
|
||||
background: url('/icons/uncap/red-hover.svg');
|
||||
}
|
||||
&:hover {
|
||||
background: url('/icons/uncap/blue-hover.svg');
|
||||
}
|
||||
}
|
||||
|
||||
&.flb {
|
||||
background: url('/icons/uncap/blue.svg');
|
||||
&.ulb {
|
||||
background: url('/icons/uncap/purple.svg');
|
||||
|
||||
&:hover {
|
||||
background: url('/icons/uncap/blue-hover.svg');
|
||||
}
|
||||
&:hover {
|
||||
background: url('/icons/uncap/purple-hover.svg');
|
||||
}
|
||||
|
||||
&.ulb {
|
||||
background: url('/icons/uncap/purple.svg');
|
||||
|
||||
&:hover {
|
||||
background: url('/icons/uncap/purple-hover.svg');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,39 +4,36 @@ import classnames from 'classnames'
|
|||
import './index.scss'
|
||||
|
||||
interface Props {
|
||||
empty: boolean
|
||||
special: boolean
|
||||
flb: boolean
|
||||
ulb: boolean
|
||||
index: number
|
||||
onClick: (index: number, empty: boolean) => void
|
||||
empty: boolean
|
||||
special: boolean
|
||||
flb: boolean
|
||||
ulb: boolean
|
||||
index: number
|
||||
onClick: (index: number, empty: boolean) => void
|
||||
}
|
||||
|
||||
const UncapStar = (props: Props) => {
|
||||
const classes = classnames({
|
||||
UncapStar: true,
|
||||
'empty': props.empty,
|
||||
'special': props.special,
|
||||
'mlb': !props.special,
|
||||
'flb': props.flb,
|
||||
'ulb': props.ulb
|
||||
const classes = classnames({
|
||||
UncapStar: true,
|
||||
empty: props.empty,
|
||||
special: props.special,
|
||||
mlb: !props.special,
|
||||
flb: props.flb,
|
||||
ulb: props.ulb,
|
||||
})
|
||||
|
||||
})
|
||||
function clicked() {
|
||||
props.onClick(props.index, props.empty)
|
||||
}
|
||||
|
||||
function clicked() {
|
||||
props.onClick(props.index, props.empty)
|
||||
}
|
||||
|
||||
return (
|
||||
<li className={classes} onClick={clicked}></li>
|
||||
)
|
||||
return <li className={classes} onClick={clicked}></li>
|
||||
}
|
||||
|
||||
UncapStar.defaultProps = {
|
||||
empty: false,
|
||||
special: false,
|
||||
flb: false,
|
||||
ulb: false
|
||||
empty: false,
|
||||
special: false,
|
||||
flb: false,
|
||||
ulb: false,
|
||||
}
|
||||
|
||||
export default UncapStar
|
||||
export default UncapStar
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue