Merge pull request #47 from jedmund/frontend-refactor

Frontend refactor
This commit is contained in:
Justin Edmund 2022-12-06 19:28:43 -08:00 committed by GitHub
commit f3255e1381
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
158 changed files with 9181 additions and 6811 deletions

1
.prettierignore Normal file
View file

@ -0,0 +1 @@
utils/api.tsx

5
.prettierrc Normal file
View file

@ -0,0 +1,5 @@
{
"semi": false,
"tabWidth": 2,
"singleQuote": true
}

View file

@ -16,9 +16,14 @@ const AboutModal = () => {
</li> </li>
</Dialog.Trigger> </Dialog.Trigger>
<Dialog.Portal> <Dialog.Portal>
<Dialog.Content className="About Dialog" onOpenAutoFocus={ (event) => event.preventDefault() }> <Dialog.Content
className="About Dialog"
onOpenAutoFocus={(event) => event.preventDefault()}
>
<div className="DialogHeader"> <div className="DialogHeader">
<Dialog.Title className="DialogTitle">{t('menu.about')}</Dialog.Title> <Dialog.Title className="DialogTitle">
{t('menu.about')}
</Dialog.Title>
<Dialog.Close className="DialogClose" asChild> <Dialog.Close className="DialogClose" asChild>
<span> <span>
<CrossIcon /> <CrossIcon />
@ -28,20 +33,27 @@ const AboutModal = () => {
<section> <section>
<Dialog.Description className="DialogDescription"> <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> 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>
<Dialog.Description className="DialogDescription"> <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. 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>
<Dialog.Description className="DialogDescription"> <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. 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> </Dialog.Description>
</section> </section>
<section> <section>
<Dialog.Title className="DialogTitle">Credits</Dialog.Title> <Dialog.Title className="DialogTitle">Credits</Dialog.Title>
<Dialog.Description className="DialogDescription"> <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>. 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> </Dialog.Description>
</section> </section>

View file

@ -19,16 +19,16 @@
height: $height; height: $height;
&:focus { &:focus {
box-shadow: 0 0 0 2px $grey-00; box-shadow: 0 0 0 2px $grey-15;
} }
&[data-state="checked"] { &[data-state='checked'] {
background: $grey-00; background: $grey-15;
} }
} }
.Thumb { .Thumb {
background: white; background: $grey-100;
border-radius: 13px; border-radius: 13px;
display: block; display: block;
height: 26px; height: 26px;
@ -40,34 +40,12 @@
cursor: pointer; cursor: pointer;
} }
&[data-state="checked"] { &[data-state='checked'] {
background: white; background: $grey-100;
transform: translateX(21px); transform: translateX(21px);
} }
} }
.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;
}
}
}
.field { .field {
align-items: center; align-items: center;
display: flex; display: flex;
@ -89,12 +67,12 @@
gap: calc($unit / 2); gap: calc($unit / 2);
label { label {
color: $grey-00; color: var(--text-secondary);
font-size: $font-regular; font-size: $font-regular;
} }
p { p {
color: $grey-60; color: var(--text-secondary);
font-size: $font-small; font-size: $font-small;
line-height: 1.1; line-height: 1.1;
max-width: 300px; max-width: 300px;
@ -118,27 +96,27 @@
} }
&.fire { &.fire {
background: $fire-bg-light; background: $fire-bg-20;
} }
&.water { &.water {
background: $water-bg-light; background: $water-bg-20;
} }
&.wind { &.wind {
background: $wind-bg-light; background: $wind-bg-20;
} }
&.earth { &.earth {
background: $earth-bg-light; background: $earth-bg-20;
} }
&.dark { &.dark {
background: $dark-bg-light; background: $dark-bg-10;
} }
&.light { &.light {
background: $light-bg-light; background: $light-bg-20;
} }
} }
} }

View file

@ -1,31 +1,31 @@
import React, { useEffect, useState } from "react" import React, { useEffect, useState } from 'react'
import { getCookie } from "cookies-next" import { getCookie } from 'cookies-next'
import { useRouter } from "next/router" import { useRouter } from 'next/router'
import { useSnapshot } from "valtio" import { useSnapshot } from 'valtio'
import { useTranslation } from "next-i18next" import { useTranslation } from 'next-i18next'
import * as Dialog from "@radix-ui/react-dialog" import * as Dialog from '@radix-ui/react-dialog'
import * as Switch from "@radix-ui/react-switch" import * as Switch from '@radix-ui/react-switch'
import api from "~utils/api" import api from '~utils/api'
import { accountState } from "~utils/accountState" import { accountState } from '~utils/accountState'
import { pictureData } from "~utils/pictureData" import { pictureData } from '~utils/pictureData'
import Button from "~components/Button" import Button from '~components/Button'
import CrossIcon from "~public/icons/Cross.svg" import CrossIcon from '~public/icons/Cross.svg'
import "./index.scss" import './index.scss'
const AccountModal = () => { const AccountModal = () => {
const { account } = useSnapshot(accountState) const { account } = useSnapshot(accountState)
const router = useRouter() const router = useRouter()
const { t } = useTranslation("common") const { t } = useTranslation('common')
const locale = const locale =
router.locale && ["en", "ja"].includes(router.locale) ? router.locale : "en" router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
// Cookies // Cookies
const cookie = getCookie("account") const cookie = getCookie('account')
const headers = {} const headers = {}
// cookies.account != null // cookies.account != null
@ -38,8 +38,8 @@ const AccountModal = () => {
// State // State
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const [picture, setPicture] = useState("") const [picture, setPicture] = useState('')
const [language, setLanguage] = useState("") const [language, setLanguage] = useState('')
const [gender, setGender] = useState(0) const [gender, setGender] = useState(0)
const [privateProfile, setPrivateProfile] = useState(false) const [privateProfile, setPrivateProfile] = useState(false)
@ -136,7 +136,7 @@ const AccountModal = () => {
<Dialog.Root open={open} onOpenChange={openChange}> <Dialog.Root open={open} onOpenChange={openChange}>
<Dialog.Trigger asChild> <Dialog.Trigger asChild>
<li className="MenuItem"> <li className="MenuItem">
<span>{t("menu.settings")}</span> <span>{t('menu.settings')}</span>
</li> </li>
</Dialog.Trigger> </Dialog.Trigger>
<Dialog.Portal> <Dialog.Portal>
@ -147,7 +147,7 @@ const AccountModal = () => {
<div className="DialogHeader"> <div className="DialogHeader">
<div className="DialogTop"> <div className="DialogTop">
<Dialog.Title className="SubTitle"> <Dialog.Title className="SubTitle">
{t("modals.settings.title")} {t('modals.settings.title')}
</Dialog.Title> </Dialog.Title>
<Dialog.Title className="DialogTitle"> <Dialog.Title className="DialogTitle">
@{account.user?.username} @{account.user?.username}
@ -163,7 +163,7 @@ const AccountModal = () => {
<form onSubmit={update}> <form onSubmit={update}>
<div className="field"> <div className="field">
<div className="left"> <div className="left">
<label>{t("modals.settings.labels.picture")}</label> <label>{t('modals.settings.labels.picture')}</label>
</div> </div>
<div <div
@ -190,7 +190,7 @@ const AccountModal = () => {
</div> </div>
<div className="field"> <div className="field">
<div className="left"> <div className="left">
<label>{t("modals.settings.labels.gender")}</label> <label>{t('modals.settings.labels.gender')}</label>
</div> </div>
<select <select
@ -200,16 +200,16 @@ const AccountModal = () => {
ref={genderSelect} ref={genderSelect}
> >
<option key="gran" value="0"> <option key="gran" value="0">
{t("modals.settings.gender.gran")} {t('modals.settings.gender.gran')}
</option> </option>
<option key="djeeta" value="1"> <option key="djeeta" value="1">
{t("modals.settings.gender.djeeta")} {t('modals.settings.gender.djeeta')}
</option> </option>
</select> </select>
</div> </div>
<div className="field"> <div className="field">
<div className="left"> <div className="left">
<label>{t("modals.settings.labels.language")}</label> <label>{t('modals.settings.labels.language')}</label>
</div> </div>
<select <select
@ -219,18 +219,18 @@ const AccountModal = () => {
ref={languageSelect} ref={languageSelect}
> >
<option key="en" value="en"> <option key="en" value="en">
{t("modals.settings.language.english")} {t('modals.settings.language.english')}
</option> </option>
<option key="jp" value="ja"> <option key="jp" value="ja">
{t("modals.settings.language.japanese")} {t('modals.settings.language.japanese')}
</option> </option>
</select> </select>
</div> </div>
<div className="field"> <div className="field">
<div className="left"> <div className="left">
<label>{t("modals.settings.labels.private")}</label> <label>{t('modals.settings.labels.private')}</label>
<p className={locale}> <p className={locale}>
{t("modals.settings.descriptions.private")} {t('modals.settings.descriptions.private')}
</p> </p>
</div> </div>
@ -243,7 +243,10 @@ const AccountModal = () => {
</Switch.Root> </Switch.Root>
</div> </div>
<Button>{t("modals.settings.buttons.confirm")}</Button> <Button
contained={true}
text={t('modals.settings.buttons.confirm')}
/>
</form> </form>
</Dialog.Content> </Dialog.Content>
<Dialog.Overlay className="Overlay" /> <Dialog.Overlay className="Overlay" />

View file

@ -11,7 +11,7 @@
} }
.Alert { .Alert {
background: white; background: $grey-100;
border-radius: $unit; border-radius: $unit;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -42,7 +42,7 @@
&:not(.btn-disabled) { &:not(.btn-disabled) {
background: $grey-90; background: $grey-90;
color: $grey-40; color: $grey-50;
&:hover { &:hover {
background: $grey-80; background: $grey-80;

View file

@ -1,9 +1,9 @@
import React from "react" import React from 'react'
import * as AlertDialog from "@radix-ui/react-alert-dialog" import * as AlertDialog from '@radix-ui/react-alert-dialog'
import "./index.scss" import './index.scss'
import Button from "~components/Button" import Button from '~components/Button'
import { ButtonType } from "~utils/enums" import { ButtonType } from '~utils/enums'
// Props // Props
interface Props { interface Props {
@ -23,7 +23,7 @@ const Alert = (props: Props) => {
<AlertDialog.Overlay className="Overlay" onClick={props.cancelAction} /> <AlertDialog.Overlay className="Overlay" onClick={props.cancelAction} />
<div className="AlertWrapper"> <div className="AlertWrapper">
<AlertDialog.Content className="Alert"> <AlertDialog.Content className="Alert">
{props.title ? <AlertDialog.Title>Error</AlertDialog.Title> : ""} {props.title ? <AlertDialog.Title>Error</AlertDialog.Title> : ''}
<AlertDialog.Description className="description"> <AlertDialog.Description className="description">
{props.message} {props.message}
</AlertDialog.Description> </AlertDialog.Description>
@ -38,7 +38,7 @@ const Alert = (props: Props) => {
{props.primaryActionText} {props.primaryActionText}
</AlertDialog.Action> </AlertDialog.Action>
) : ( ) : (
"" ''
)} )}
</div> </div>
</AlertDialog.Content> </AlertDialog.Content>

View file

@ -34,7 +34,7 @@
background-color: $grey-90; background-color: $grey-90;
border-radius: 6px; border-radius: 6px;
box-sizing: border-box; box-sizing: border-box;
color: $grey-00; color: $grey-15;
height: $unit * 6; height: $unit * 6;
display: block; display: block;
font-size: $font-regular; font-size: $font-regular;

View file

@ -16,30 +16,36 @@ interface ErrorMap {
interface Props { interface Props {
axType: number axType: number
currentSkills?: SimpleAxSkill[], currentSkills?: SimpleAxSkill[]
sendValidity: (isValid: boolean) => void sendValidity: (isValid: boolean) => void
sendValues: (primaryAxModifier: number, primaryAxValue: number, secondaryAxModifier: number, secondaryAxValue: number) => void sendValues: (
primaryAxModifier: number,
primaryAxValue: number,
secondaryAxModifier: number,
secondaryAxValue: number
) => void
} }
const AXSelect = (props: Props) => { const AXSelect = (props: Props) => {
const router = useRouter() const router = useRouter()
const locale = (router.locale && ['en', 'ja'].includes(router.locale)) ? router.locale : 'en' const locale =
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
const { t } = useTranslation('common') const { t } = useTranslation('common')
// Set up form states and error handling // Set up form states and error handling
const [errors, setErrors] = useState<ErrorMap>({ const [errors, setErrors] = useState<ErrorMap>({
axValue1: '', axValue1: '',
axValue2: '' axValue2: '',
}) })
const primaryErrorClasses = classNames({ const primaryErrorClasses = classNames({
'errors': true, errors: true,
'visible': errors.axValue1.length > 0 visible: errors.axValue1.length > 0,
}) })
const secondaryErrorClasses = classNames({ const secondaryErrorClasses = classNames({
'errors': true, errors: true,
'visible': errors.axValue2.length > 0 visible: errors.axValue2.length > 0,
}) })
// Refs // Refs
@ -71,17 +77,30 @@ const AXSelect = (props: Props) => {
}, [props.currentSkills]) }, [props.currentSkills])
useEffect(() => { useEffect(() => {
props.sendValues(primaryAxModifier, primaryAxValue, secondaryAxModifier, secondaryAxValue) props.sendValues(
}, [props, primaryAxModifier, primaryAxValue, secondaryAxModifier, secondaryAxValue]) primaryAxModifier,
primaryAxValue,
secondaryAxModifier,
secondaryAxValue
)
}, [
props,
primaryAxModifier,
primaryAxValue,
secondaryAxModifier,
secondaryAxValue,
])
useEffect(() => { useEffect(() => {
props.sendValidity(primaryAxValue > 0 && errors.axValue1 === '' && errors.axValue2 === '') props.sendValidity(
primaryAxValue > 0 && errors.axValue1 === '' && errors.axValue2 === ''
)
}, [props, primaryAxValue, errors]) }, [props, primaryAxValue, errors])
// Classes // Classes
const secondarySetClasses = classNames({ const secondarySetClasses = classNames({
'AXSet': true, AXSet: true,
'hidden': primaryAxModifier < 0 hidden: primaryAxModifier < 0,
}) })
function generateOptions(modifierSet: number) { function generateOptions(modifierSet: number) {
@ -91,17 +110,17 @@ const AXSelect = (props: Props) => {
if (modifierSet == 0) { if (modifierSet == 0) {
axOptionElements = axOptions.map((ax, i) => { axOptionElements = axOptions.map((ax, i) => {
return ( return (
<option key={i} value={ax.id}>{ax.name[locale]}</option> <option key={i} value={ax.id}>
{ax.name[locale]}
</option>
) )
}) })
} else { } else {
// If we are loading data from the server, state doesn't set before render, // If we are loading data from the server, state doesn't set before render,
// so our defaultValue is undefined. // so our defaultValue is undefined.
let modifier = -1; let modifier = -1
if (primaryAxModifier >= 0) if (primaryAxModifier >= 0) modifier = primaryAxModifier
modifier = primaryAxModifier else if (props.currentSkills) modifier = props.currentSkills[0].modifier
else if (props.currentSkills)
modifier = props.currentSkills[0].modifier
if (modifier >= 0 && axOptions[modifier]) { if (modifier >= 0 && axOptions[modifier]) {
const primarySkill = axOptions[modifier] const primarySkill = axOptions[modifier]
@ -110,14 +129,20 @@ const AXSelect = (props: Props) => {
const secondaryAxOptions = primarySkill.secondary const secondaryAxOptions = primarySkill.secondary
axOptionElements = secondaryAxOptions.map((ax, i) => { axOptionElements = secondaryAxOptions.map((ax, i) => {
return ( return (
<option key={i} value={ax.id}>{ax.name[locale]}</option> <option key={i} value={ax.id}>
{ax.name[locale]}
</option>
) )
}) })
} }
} }
} }
axOptionElements?.unshift(<option key={-1} value={-1}>{t('ax.no_skill')}</option>) axOptionElements?.unshift(
<option key={-1} value={-1}>
{t('ax.no_skill')}
</option>
)
return axOptionElements return axOptionElements
} }
@ -127,18 +152,23 @@ const AXSelect = (props: Props) => {
if (primaryAxModifierSelect.current == event.target) { if (primaryAxModifierSelect.current == event.target) {
setPrimaryAxModifier(value) setPrimaryAxModifier(value)
if (primaryAxValueInput.current && secondaryAxModifierSelect.current && secondaryAxValueInput.current) { if (
primaryAxValueInput.current &&
secondaryAxModifierSelect.current &&
secondaryAxValueInput.current
) {
setupInput(axData[props.axType - 1][value], primaryAxValueInput.current) setupInput(axData[props.axType - 1][value], primaryAxValueInput.current)
secondaryAxModifierSelect.current.value = "-1" secondaryAxModifierSelect.current.value = '-1'
secondaryAxValueInput.current.value = "" secondaryAxValueInput.current.value = ''
} }
} else { } else {
setSecondaryAxModifier(value) setSecondaryAxModifier(value)
const primaryAxSkill = axData[props.axType - 1][primaryAxModifier] const primaryAxSkill = axData[props.axType - 1][primaryAxModifier]
const currentAxSkill = (primaryAxSkill.secondary) ? const currentAxSkill = primaryAxSkill.secondary
primaryAxSkill.secondary.find(skill => skill.id == value) : undefined ? primaryAxSkill.secondary.find((skill) => skill.id == value)
: undefined
if (secondaryAxValueInput.current) if (secondaryAxValueInput.current)
setupInput(currentAxSkill, secondaryAxValueInput.current) setupInput(currentAxSkill, secondaryAxValueInput.current)
@ -147,35 +177,35 @@ const AXSelect = (props: Props) => {
function handleInputChange(event: React.ChangeEvent<HTMLInputElement>) { function handleInputChange(event: React.ChangeEvent<HTMLInputElement>) {
const value = parseFloat(event.target.value) const value = parseFloat(event.target.value)
let newErrors = {...errors} let newErrors = { ...errors }
if (primaryAxValueInput.current == event.target) { if (primaryAxValueInput.current == event.target) {
if (handlePrimaryErrors(value)) if (handlePrimaryErrors(value)) setPrimaryAxValue(value)
setPrimaryAxValue(value)
} else { } else {
if (handleSecondaryErrors(value)) if (handleSecondaryErrors(value)) setSecondaryAxValue(value)
setSecondaryAxValue(value)
} }
} }
function handlePrimaryErrors(value: number) { function handlePrimaryErrors(value: number) {
const primaryAxSkill = axData[props.axType - 1][primaryAxModifier] const primaryAxSkill = axData[props.axType - 1][primaryAxModifier]
let newErrors = {...errors} let newErrors = { ...errors }
if (value < primaryAxSkill.minValue) { if (value < primaryAxSkill.minValue) {
newErrors.axValue1 = t('ax.errors.value_too_low', { newErrors.axValue1 = t('ax.errors.value_too_low', {
name: primaryAxSkill.name[locale], name: primaryAxSkill.name[locale],
minValue: primaryAxSkill.minValue, minValue: primaryAxSkill.minValue,
suffix: (primaryAxSkill.suffix) ? primaryAxSkill.suffix : '' suffix: primaryAxSkill.suffix ? primaryAxSkill.suffix : '',
}) })
} else if (value > primaryAxSkill.maxValue) { } else if (value > primaryAxSkill.maxValue) {
newErrors.axValue1 = t('ax.errors.value_too_high', { newErrors.axValue1 = t('ax.errors.value_too_high', {
name: primaryAxSkill.name[locale], name: primaryAxSkill.name[locale],
maxValue: primaryAxSkill.minValue, maxValue: primaryAxSkill.minValue,
suffix: (primaryAxSkill.suffix) ? primaryAxSkill.suffix : '' suffix: primaryAxSkill.suffix ? primaryAxSkill.suffix : '',
}) })
} else if (!value || value <= 0) { } else if (!value || value <= 0) {
newErrors.axValue1 = t('ax.errors.value_empty', { name: primaryAxSkill.name[locale] }) newErrors.axValue1 = t('ax.errors.value_empty', {
name: primaryAxSkill.name[locale],
})
} else { } else {
newErrors.axValue1 = '' newErrors.axValue1 = ''
} }
@ -187,28 +217,34 @@ const AXSelect = (props: Props) => {
function handleSecondaryErrors(value: number) { function handleSecondaryErrors(value: number) {
const primaryAxSkill = axData[props.axType - 1][primaryAxModifier] const primaryAxSkill = axData[props.axType - 1][primaryAxModifier]
let newErrors = {...errors} let newErrors = { ...errors }
if (primaryAxSkill.secondary) { if (primaryAxSkill.secondary) {
const secondaryAxSkill = primaryAxSkill.secondary.find(skill => skill.id == secondaryAxModifier) const secondaryAxSkill = primaryAxSkill.secondary.find(
(skill) => skill.id == secondaryAxModifier
)
if (secondaryAxSkill) { if (secondaryAxSkill) {
if (value < secondaryAxSkill.minValue) { if (value < secondaryAxSkill.minValue) {
newErrors.axValue2 = t('ax.errors.value_too_low', { newErrors.axValue2 = t('ax.errors.value_too_low', {
name: secondaryAxSkill.name[locale], name: secondaryAxSkill.name[locale],
minValue: secondaryAxSkill.minValue, minValue: secondaryAxSkill.minValue,
suffix: (secondaryAxSkill.suffix) ? secondaryAxSkill.suffix : '' suffix: secondaryAxSkill.suffix ? secondaryAxSkill.suffix : '',
}) })
} else if (value > secondaryAxSkill.maxValue) { } else if (value > secondaryAxSkill.maxValue) {
newErrors.axValue2 = t('ax.errors.value_too_high', { newErrors.axValue2 = t('ax.errors.value_too_high', {
name: secondaryAxSkill.name[locale], name: secondaryAxSkill.name[locale],
maxValue: secondaryAxSkill.minValue, maxValue: secondaryAxSkill.minValue,
suffix: (secondaryAxSkill.suffix) ? secondaryAxSkill.suffix : '' suffix: secondaryAxSkill.suffix ? secondaryAxSkill.suffix : '',
}) })
} else if (!secondaryAxSkill.suffix && value % 1 !== 0) { } else if (!secondaryAxSkill.suffix && value % 1 !== 0) {
newErrors.axValue2 = t('ax.errors.value_not_whole', { name: secondaryAxSkill.name[locale] }) newErrors.axValue2 = t('ax.errors.value_not_whole', {
name: secondaryAxSkill.name[locale],
})
} else if (primaryAxValue <= 0) { } else if (primaryAxValue <= 0) {
newErrors.axValue1 = t('ax.errors.value_empty', { name: primaryAxSkill.name[locale] }) newErrors.axValue1 = t('ax.errors.value_empty', {
name: primaryAxSkill.name[locale],
})
} else { } else {
newErrors.axValue2 = '' newErrors.axValue2 = ''
} }
@ -228,7 +264,7 @@ const AXSelect = (props: Props) => {
element.placeholder = rangeString element.placeholder = rangeString
element.min = `${ax.minValue}` element.min = `${ax.minValue}`
element.max = `${ax.maxValue}` element.max = `${ax.maxValue}`
element.step = (ax.suffix) ? "0.5" : "1" element.step = ax.suffix ? '0.5' : '1'
} else { } else {
if (primaryAxValueInput.current && secondaryAxValueInput.current) { if (primaryAxValueInput.current && secondaryAxValueInput.current) {
if (primaryAxValueInput.current == element) { if (primaryAxValueInput.current == element) {
@ -246,16 +282,60 @@ const AXSelect = (props: Props) => {
<div className="AXSelect"> <div className="AXSelect">
<div className="AXSet"> <div className="AXSet">
<div className="fields"> <div className="fields">
<select key="ax1" defaultValue={ (props.currentSkills && props.currentSkills[0]) ? props.currentSkills[0].modifier : -1 } onChange={handleSelectChange} ref={primaryAxModifierSelect}>{ generateOptions(0) }</select> <select
<input defaultValue={ (props.currentSkills && props.currentSkills[0]) ? props.currentSkills[0].strength : 0 } className="Input" type="number" onChange={handleInputChange} ref={primaryAxValueInput} disabled={primaryAxValue != 0} /> 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> </div>
<p className={primaryErrorClasses}>{errors.axValue1}</p> <p className={primaryErrorClasses}>{errors.axValue1}</p>
</div> </div>
<div className={secondarySetClasses}> <div className={secondarySetClasses}>
<div className="fields"> <div className="fields">
<select key="ax2" defaultValue={ (props.currentSkills && props.currentSkills[1]) ? props.currentSkills[1].modifier : -1 } onChange={handleSelectChange} ref={secondaryAxModifierSelect}>{ generateOptions(1) }</select> <select
<input defaultValue={ (props.currentSkills && props.currentSkills[1]) ? props.currentSkills[1].strength : 0 } className="Input" type="number" onChange={handleInputChange} ref={secondaryAxValueInput} disabled={secondaryAxValue != 0} /> 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> </div>
<p className={secondaryErrorClasses}>{errors.axValue2}</p> <p className={secondaryErrorClasses}>{errors.axValue2}</p>
</div> </div>

View file

@ -1,62 +1,101 @@
.Button { .Button {
align-items: center; align-items: center;
background: transparent; background: var(--button-bg);
border: none; border: none;
border-radius: 6px; border-radius: $input-corner;
color: $grey-50; color: var(--button-text);
display: inline-flex; display: inline-flex;
font-size: $font-button; font-size: $font-button;
font-weight: $normal; font-weight: $normal;
gap: 6px; gap: 6px;
padding: 8px 12px;
&: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;
}
&.Contained {
background: var(--button-contained-bg);
&:hover { &:hover {
background: white; background: var(--button-contained-bg-hover);
cursor: pointer;
color: $grey-00;
.icon svg {
fill: $grey-00;
} }
.icon.stroke svg { &.Save:hover .Accessory svg {
fill: none; fill: #ff4d4d;
stroke: $grey-00; 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 { &.destructive:hover {
background: $error; background: $error;
color: white; color: $grey-100;
.icon svg { .Accessory svg {
fill: white; fill: $grey-100;
} }
} }
&.save:hover { &.save:hover {
color: #FF4D4D; color: #ff4d4d;
.icon svg { .Accessory svg {
fill: #FF4D4D; fill: #ff4d4d;
stroke: #FF4D4D; stroke: #ff4d4d;
} }
} }
&.save.Active { &.save.Active {
color: #FF4D4D; color: #ff4d4d;
.icon svg {
fill: #FF4D4D;
stroke: #FF4D4D;
}
&:hover { &:hover {
color: darken(#FF4D4D, 30); color: darken(#ff4d4d, 30);
.icon svg { .icon svg {
fill: darken(#FF4D4D, 30); fill: darken(#ff4d4d, 30);
stroke: darken(#FF4D4D, 30); stroke: darken(#ff4d4d, 30);
} }
} }
} }
@ -69,17 +108,34 @@
color: $error; color: $error;
&:hover { &:hover {
color: darken($error, 10) color: darken($error, 10);
} }
} }
.icon { .Accessory {
margin-top: 2px; $dimension: $unit-2x;
display: flex;
svg { svg {
fill: $grey-50; fill: var(--button-text);
height: 12px; height: $dimension;
width: 12px; width: $dimension;
&.stroke {
fill: none;
stroke: var(--button-text);
}
&.Add {
height: 18px;
width: 18px;
}
&.Check {
height: 22px;
width: 22px;
}
} }
&.check svg { &.check svg {
@ -88,28 +144,19 @@
width: auto; width: auto;
} }
&.stroke svg { svg &.settings svg {
fill: none;
stroke: $grey-50;
}
&.settings svg {
height: 13px; height: 13px;
width: 13px; width: 13px;
} }
} }
&.Active {
background: white;
}
&.btn-blue { &.btn-blue {
background: $blue; background: $blue;
color: #8b8b8b; color: #8b8b8b;
&:hover { &:hover {
background: #4B9BE5; background: #4b9be5;
color: #233E56; color: #233e56;
} }
} }
@ -143,70 +190,69 @@
&.null { &.null {
background: $grey-90; background: $grey-90;
color: $grey-50; color: $grey-55;
&:hover { &:hover {
background: $grey-70; background: $grey-70;
color: $grey-00; color: $grey-15;
} }
} }
&.wind { &.wind {
background: $wind-bg-light; background: $wind-bg-20;
color: $wind-text-dark; color: $wind-text-10;
&:hover { &:hover {
background: darken($wind-bg-light, 10); background: darken($wind-bg-20, 10);
} }
} }
&.fire { &.fire {
background: $fire-bg-light; background: $fire-bg-20;
color: $fire-text-dark; color: $fire-text-10;
&:hover { &:hover {
background: darken($fire-bg-light, 10); background: darken($fire-bg-20, 10);
} }
} }
&.water { &.water {
background: $water-bg-light; background: $water-bg-20;
color: $water-text-dark; color: $water-text-10;
&:hover { &:hover {
background: darken($water-bg-light, 10); background: darken($water-bg-20, 10);
} }
} }
&.earth { &.earth {
background: $earth-bg-light; background: $earth-bg-20;
color: $earth-text-dark; color: $earth-text-10;
&:hover { &:hover {
background: darken($earth-bg-light, 10); background: darken($earth-bg-20, 10);
} }
} }
&.dark { &.dark {
background: $dark-bg-light; background: $dark-bg-10;
color: $dark-text-dark; color: $dark-text-10;
&:hover { &:hover {
background: darken($dark-bg-light, 10); background: darken($dark-bg-10, 10);
} }
} }
&.light { &.light {
background: $light-bg-light; background: $light-bg-20;
color: $light-text-dark; color: $light-text-10;
&:hover { &:hover {
background: darken($light-bg-light, 10); background: darken($light-bg-20, 10);
} }
} }
.text { .Text {
color: inherit; color: inherit;
display: block; display: block;
width: 100%; width: 100%;

View file

@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react' import React, { PropsWithChildren, useEffect, useState } from 'react'
import classNames from 'classnames' import classNames from 'classnames'
import Link from 'next/link' import Link from 'next/link'
@ -15,127 +15,174 @@ import SettingsIcon from '~public/icons/Settings.svg'
import './index.scss' import './index.scss'
import { ButtonType } from '~utils/enums' import { ButtonType } from '~utils/enums'
import { access } from 'fs'
interface Props { interface Props
extends React.DetailedHTMLProps<
React.ButtonHTMLAttributes<HTMLButtonElement>,
HTMLButtonElement
> {
accessoryIcon?: React.ReactNode
active?: boolean active?: boolean
disabled?: boolean blended?: boolean
classes?: string[], contained?: boolean
icon?: string size?: 'small' | 'medium' | 'large'
type?: ButtonType text?: string
children?: React.ReactNode
onClick?: (event: React.MouseEvent<HTMLElement>) => void
} }
const Button = (props: Props) => { const defaultProps = {
// States active: false,
const [active, setActive] = useState(false) blended: false,
const [disabled, setDisabled] = useState(false) contained: false,
const [pressed, setPressed] = useState(false) size: 'medium',
const [buttonType, setButtonType] = useState(ButtonType.Base) }
const classes = classNames({ const Button = React.forwardRef<HTMLButtonElement, Props>(function button(
{ accessoryIcon, active, blended, contained, size, text, ...props },
forwardedRef
) {
const classes = classNames(
{
Button: true, Button: true,
'Active': active, Active: active,
'btn-pressed': pressed, Blended: blended,
'btn-disabled': disabled, Contained: contained,
'save': props.icon === 'save', // 'btn-pressed': pressed,
'destructive': props.type == ButtonType.Destructive // 'btn-disabled': disabled,
}, props.classes) // save: props.icon === 'save',
// destructive: props.type == ButtonType.Destructive,
useEffect(() => { },
if (props.active) setActive(props.active) size,
if (props.disabled) setDisabled(props.disabled) props.className
if (props.type) setButtonType(props.type)
}, [props.active, props.disabled, props.type])
const addIcon = (
<span className='icon'>
<AddIcon />
</span>
) )
const menuIcon = ( const hasAccessory = () => {
<span className='icon'> if (accessoryIcon) return <span className="Accessory">{accessoryIcon}</span>
<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 const hasText = () => {
if (text) return <span className="Text">{text}</span>
} }
function handleMouseDown() {
setPressed(true)
}
function handleMouseUp() {
setPressed(false)
}
return ( return (
<button <button {...props} className={classes} ref={forwardedRef}>
className={classes} {hasAccessory()}
disabled={disabled} {hasText()}
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
onClick={props.onClick}>
{ getIcon() }
{ (props.type != ButtonType.IconOnly) ?
<span className='text'>
{ props.children }
</span> : ''
}
</button> </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 export default Button

View file

@ -1,19 +1,23 @@
.Limited { .Limited {
background: white; $offset: 2px;
border-radius: 6px;
border: 2px solid transparent; background: var(--input-bg);
border-radius: $input-corner;
border: $offset solid transparent;
box-sizing: border-box; box-sizing: border-box;
display: flex; display: flex;
gap: $unit; gap: $unit;
padding-right: $unit * 2; padding-top: 2px;
padding-bottom: 2px;
padding-right: calc($unit-2x - $offset);
&:focus-within { &:focus-within {
border: 2px solid $blue; border: $offset solid $blue;
box-shadow: 0 2px rgba(255, 255, 255, 1); // box-shadow: 0 2px rgba(255, 255, 255, 1);
} }
.Counter { .Counter {
color: $grey-50; color: $grey-55;
font-weight: $bold; font-weight: $bold;
line-height: 42px; line-height: 42px;
} }
@ -21,6 +25,7 @@
.Input { .Input {
background: transparent; background: transparent;
border-radius: 0; border-radius: 0;
padding-left: calc($unit-2x - $offset);
&:focus { &:focus {
outline: none; outline: none;

View file

@ -11,13 +11,18 @@ interface Props {
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void
} }
const CharLimitedFieldset = React.forwardRef<HTMLInputElement, Props>(function useFieldSet(props, ref) { const CharLimitedFieldset = React.forwardRef<HTMLInputElement, Props>(
const fieldType = (['password', 'confirm_password'].includes(props.fieldName)) ? 'password' : 'text' function useFieldSet(props, ref) {
const fieldType = ['password', 'confirm_password'].includes(props.fieldName)
? 'password'
: 'text'
const [currentCount, setCurrentCount] = useState(0) const [currentCount, setCurrentCount] = useState(0)
useEffect(() => { 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]) }, [props.limit, props.value])
function onChange(event: React.ChangeEvent<HTMLInputElement>) { function onChange(event: React.ChangeEvent<HTMLInputElement>) {
@ -43,12 +48,10 @@ const CharLimitedFieldset = React.forwardRef<HTMLInputElement, Props>(function u
/> />
<span className="Counter">{currentCount}</span> <span className="Counter">{currentCount}</span>
</div> </div>
{ {props.error.length > 0 && <p className="InputError">{props.error}</p>}
props.error.length > 0 &&
<p className='InputError'>{props.error}</p>
}
</fieldset> </fieldset>
) )
}) }
)
export default CharLimitedFieldset export default CharLimitedFieldset

View file

@ -8,7 +8,7 @@
} }
.arrow { .arrow {
color: $grey-50; color: $grey-55;
font-size: 4rem; font-size: 4rem;
text-align: center; text-align: center;
} }
@ -52,7 +52,7 @@
&:not(.btn-disabled) { &:not(.btn-disabled) {
background: $grey-90; background: $grey-90;
color: $grey-40; color: $grey-50;
&:hover { &:hover {
background: $grey-80; background: $grey-80;

View file

@ -1,18 +1,18 @@
import React, { useEffect, useState } from "react" import React, { useEffect, useState } from 'react'
import { setCookie } from "cookies-next" import { setCookie } from 'cookies-next'
import Router, { useRouter } from "next/router" import Router, { useRouter } from 'next/router'
import { useTranslation } from "react-i18next" import { useTranslation } from 'react-i18next'
import { AxiosResponse } from "axios" 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 api from '~utils/api'
import { appState } from "~utils/appState" import { appState } from '~utils/appState'
import { accountState } from "~utils/accountState" import { accountState } from '~utils/accountState'
import Button from "~components/Button" import Button from '~components/Button'
import "./index.scss" import './index.scss'
interface Props { interface Props {
open: boolean open: boolean
@ -24,7 +24,7 @@ interface Props {
} }
const CharacterConflictModal = (props: Props) => { const CharacterConflictModal = (props: Props) => {
const { t } = useTranslation("common") const { t } = useTranslation('common')
// States // States
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
@ -35,13 +35,13 @@ const CharacterConflictModal = (props: Props) => {
function imageUrl(character?: Character, uncap: number = 0) { function imageUrl(character?: Character, uncap: number = 0) {
// Change the image based on the uncap level // Change the image based on the uncap level
let suffix = "01" let suffix = '01'
if (uncap == 6) suffix = "04" if (uncap == 6) suffix = '04'
else if (uncap == 5) suffix = "03" else if (uncap == 5) suffix = '03'
else if (uncap > 2) suffix = "02" else if (uncap > 2) suffix = '02'
// Special casing for Lyria (and Young Cat eventually) // Special casing for Lyria (and Young Cat eventually)
if (character?.granblue_id === "3030182000") { if (character?.granblue_id === '3030182000') {
let element = 1 let element = 1
if ( if (
appState.grid.weapons.mainWeapon && appState.grid.weapons.mainWeapon &&

View file

@ -1,22 +1,22 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
import React, { useCallback, useEffect, useMemo, useState } from "react" import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { getCookie } from "cookies-next" import { getCookie } from 'cookies-next'
import { useSnapshot } from "valtio" import { useSnapshot } from 'valtio'
import { AxiosResponse } from "axios" import { AxiosResponse } from 'axios'
import debounce from "lodash.debounce" import debounce from 'lodash.debounce'
import Alert from "~components/Alert" import Alert from '~components/Alert'
import JobSection from "~components/JobSection" import JobSection from '~components/JobSection'
import CharacterUnit from "~components/CharacterUnit" import CharacterUnit from '~components/CharacterUnit'
import CharacterConflictModal from "~components/CharacterConflictModal" import CharacterConflictModal from '~components/CharacterConflictModal'
import type { JobSkillObject, SearchableObject } from "~types" import type { JobSkillObject, SearchableObject } from '~types'
import api from "~utils/api" import api from '~utils/api'
import { appState } from "~utils/appState" import { appState } from '~utils/appState'
import "./index.scss" import './index.scss'
// Props // Props
interface Props { interface Props {
@ -31,7 +31,7 @@ const CharacterGrid = (props: Props) => {
const numCharacters: number = 5 const numCharacters: number = 5
// Cookies // Cookies
const cookie = getCookie("account") const cookie = getCookie('account')
const accountData: AccountCookie = cookie const accountData: AccountCookie = cookie
? JSON.parse(cookie as string) ? JSON.parse(cookie as string)
: null : null
@ -57,7 +57,7 @@ const CharacterGrid = (props: Props) => {
2: undefined, 2: undefined,
3: undefined, 3: undefined,
}) })
const [errorMessage, setErrorMessage] = useState("") const [errorMessage, setErrorMessage] = useState('')
// Create a temporary state to store previous character uncap values // Create a temporary state to store previous character uncap values
const [previousUncapValues, setPreviousUncapValues] = useState<{ const [previousUncapValues, setPreviousUncapValues] = useState<{
@ -116,7 +116,7 @@ const CharacterGrid = (props: Props) => {
} }
async function handleCharacterResponse(data: any) { async function handleCharacterResponse(data: any) {
if (data.hasOwnProperty("conflicts")) { if (data.hasOwnProperty('conflicts')) {
setIncoming(data.incoming) setIncoming(data.incoming)
setConflicts(data.conflicts) setConflicts(data.conflicts)
setPosition(data.position) setPosition(data.position)
@ -185,7 +185,7 @@ const CharacterGrid = (props: Props) => {
const saveJob = function (job: Job) { const saveJob = function (job: Job) {
const payload = { const payload = {
party: { party: {
job_id: job ? job.id : "", job_id: job ? job.id : '',
}, },
...headers, ...headers,
} }
@ -231,9 +231,9 @@ const CharacterGrid = (props: Props) => {
}) })
.catch((error) => { .catch((error) => {
const data = error.response.data 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 ${ const message = `You can only add up to 2 ${
data.skill_type === "emp" data.skill_type === 'emp'
? data.skill_type.toUpperCase() ? data.skill_type.toUpperCase()
: data.skill_type : data.skill_type
} skills to your party at once.` } skills to your party at once.`
@ -268,7 +268,7 @@ const CharacterGrid = (props: Props) => {
try { try {
if (uncapLevel != previousUncapValues[position]) 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) storeGridCharacter(response.data.grid_character)
}) })
} catch (error) { } catch (error) {
@ -332,7 +332,7 @@ const CharacterGrid = (props: Props) => {
} }
function cancelAlert() { function cancelAlert() {
setErrorMessage("") setErrorMessage('')
} }
// Render: JSX components // Render: JSX components
@ -342,7 +342,7 @@ const CharacterGrid = (props: Props) => {
open={errorMessage.length > 0} open={errorMessage.length > 0}
message={errorMessage} message={errorMessage}
cancelAction={cancelAlert} cancelAction={cancelAlert}
cancelActionText={"Got it"} cancelActionText={'Got it'}
/> />
<div id="CharacterGrid"> <div id="CharacterGrid">
<JobSection <JobSection

View file

@ -16,7 +16,7 @@ interface Props {
interface KeyNames { interface KeyNames {
[key: string]: { [key: string]: {
en: string, en: string
jp: string jp: string
} }
} }
@ -24,28 +24,41 @@ interface KeyNames {
const CharacterHovercard = (props: Props) => { const CharacterHovercard = (props: Props) => {
const router = useRouter() const router = useRouter()
const { t } = useTranslation('common') const { t } = useTranslation('common')
const locale = (router.locale && ['en', 'ja'].includes(router.locale)) ? router.locale : 'en' 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 Proficiency = ['none', 'sword', 'dagger', 'axe', 'spear', 'bow', 'staff', 'fist', 'harp', 'gun', 'katana'] const Proficiency = [
'none',
'sword',
'dagger',
'axe',
'spear',
'bow',
'staff',
'fist',
'harp',
'gun',
'katana',
]
const tintElement = Element[props.gridCharacter.object.element] const tintElement = Element[props.gridCharacter.object.element]
const wikiUrl = `https://gbf.wiki/${props.gridCharacter.object.name.en.replaceAll(' ', '_')}` const wikiUrl = `https://gbf.wiki/${props.gridCharacter.object.name.en.replaceAll(
' ',
'_'
)}`
function characterImage() { function characterImage() {
let imgSrc = "" let imgSrc = ''
if (props.gridCharacter) { if (props.gridCharacter) {
const character = props.gridCharacter.object const character = props.gridCharacter.object
// Change the image based on the uncap level // Change the image based on the uncap level
let suffix = '01' let suffix = '01'
if (props.gridCharacter.uncap_level == 6) if (props.gridCharacter.uncap_level == 6) suffix = '04'
suffix = '04' else if (props.gridCharacter.uncap_level == 5) suffix = '03'
else if (props.gridCharacter.uncap_level == 5) else if (props.gridCharacter.uncap_level > 2) suffix = '02'
suffix = '03'
else if (props.gridCharacter.uncap_level > 2)
suffix = '02'
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/chara-grid/${character.granblue_id}_${suffix}.jpg` imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/chara-grid/${character.granblue_id}_${suffix}.jpg`
} }
@ -55,22 +68,39 @@ const CharacterHovercard = (props: Props) => {
return ( return (
<HoverCard.Root> <HoverCard.Root>
<HoverCard.Trigger> <HoverCard.Trigger>{props.children}</HoverCard.Trigger>
{ props.children }
</HoverCard.Trigger>
<HoverCard.Content className="Weapon Hovercard"> <HoverCard.Content className="Weapon Hovercard">
<div className="top"> <div className="top">
<div className="title"> <div className="title">
<h4>{ props.gridCharacter.object.name[locale] }</h4> <h4>{props.gridCharacter.object.name[locale]}</h4>
<img alt={props.gridCharacter.object.name[locale]} src={characterImage()} /> <img
alt={props.gridCharacter.object.name[locale]}
src={characterImage()}
/>
</div> </div>
<div className="subInfo"> <div className="subInfo">
<div className="icons"> <div className="icons">
<WeaponLabelIcon labelType={Element[props.gridCharacter.object.element]} /> <WeaponLabelIcon
<WeaponLabelIcon labelType={ Proficiency[props.gridCharacter.object.proficiency.proficiency1] } /> labelType={Element[props.gridCharacter.object.element]}
{ (props.gridCharacter.object.proficiency.proficiency2) ? />
<WeaponLabelIcon labelType={ Proficiency[props.gridCharacter.object.proficiency.proficiency2] } /> <WeaponLabelIcon
: ''} labelType={
Proficiency[
props.gridCharacter.object.proficiency.proficiency1
]
}
/>
{props.gridCharacter.object.proficiency.proficiency2 ? (
<WeaponLabelIcon
labelType={
Proficiency[
props.gridCharacter.object.proficiency.proficiency2
]
}
/>
) : (
''
)}
</div> </div>
<UncapIndicator <UncapIndicator
type="character" type="character"
@ -81,7 +111,9 @@ const CharacterHovercard = (props: Props) => {
</div> </div>
</div> </div>
<a className={`Button ${tintElement}`} href={wikiUrl} target="_new">{t('buttons.wiki')}</a> <a className={`Button ${tintElement}`} href={wikiUrl} target="_new">
{t('buttons.wiki')}
</a>
<HoverCard.Arrow /> <HoverCard.Arrow />
</HoverCard.Content> </HoverCard.Content>
</HoverCard.Root> </HoverCard.Root>
@ -89,4 +121,3 @@ const CharacterHovercard = (props: Props) => {
} }
export default CharacterHovercard export default CharacterHovercard

View file

@ -5,12 +5,16 @@
padding: $unit * 1.5; padding: $unit * 1.5;
&:hover { &:hover {
background: $grey-90; background: var(--button-contained-bg);
cursor: pointer; cursor: pointer;
.Info h5 {
color: var(--text-primary);
}
} }
img { img {
background: $grey-80; background: var(--card-bg);
border-radius: 6px; border-radius: 6px;
display: inline-block; display: inline-block;
height: 72px; height: 72px;
@ -21,10 +25,10 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex-grow: 1; flex-grow: 1;
gap: calc($unit / 2); gap: $unit-half;
h5 { h5 {
color: #555; color: var(--text-secondary);
display: inline-block; display: inline-block;
font-size: $font-medium; font-size: $font-medium;
font-weight: $medium; font-weight: $medium;
@ -37,11 +41,11 @@
.stars { .stars {
display: inline-block; display: inline-block;
color: #FFA15E; color: #ffa15e;
font-size: $font-xlarge; font-size: $font-xlarge;
& > span { & > span {
color: #65DAFF; color: #65daff;
} }
} }

View file

@ -15,7 +15,8 @@ const Element = ['null', 'wind', 'fire', 'water', 'earth', 'dark', 'light']
const CharacterResult = (props: Props) => { const CharacterResult = (props: Props) => {
const router = useRouter() const router = useRouter()
const locale = (router.locale && ['en', 'ja'].includes(router.locale)) ? router.locale : 'en' const locale =
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
const character = props.data const character = props.data

View file

@ -9,7 +9,11 @@ import SearchFilter from '~components/SearchFilter'
import SearchFilterCheckboxItem from '~components/SearchFilterCheckboxItem' import SearchFilterCheckboxItem from '~components/SearchFilterCheckboxItem'
import './index.scss' import './index.scss'
import { emptyElementState, emptyProficiencyState, emptyRarityState } from '~utils/emptyStates' import {
emptyElementState,
emptyProficiencyState,
emptyRarityState,
} from '~utils/emptyStates'
import { elements, proficiencies, rarities } from '~utils/stateValues' import { elements, proficiencies, rarities } from '~utils/stateValues'
interface Props { interface Props {
@ -25,9 +29,14 @@ const CharacterSearchFilterBar = (props: Props) => {
const [proficiency2Menu, setProficiency2Menu] = useState(false) const [proficiency2Menu, setProficiency2Menu] = useState(false)
const [rarityState, setRarityState] = useState<RarityState>(emptyRarityState) const [rarityState, setRarityState] = useState<RarityState>(emptyRarityState)
const [elementState, setElementState] = useState<ElementState>(emptyElementState) const [elementState, setElementState] =
const [proficiency1State, setProficiency1State] = useState<ProficiencyState>(emptyProficiencyState) useState<ElementState>(emptyElementState)
const [proficiency2State, setProficiency2State] = useState<ProficiencyState>(emptyProficiencyState) const [proficiency1State, setProficiency1State] = useState<ProficiencyState>(
emptyProficiencyState
)
const [proficiency2State, setProficiency2State] = useState<ProficiencyState>(
emptyProficiencyState
)
function rarityMenuOpened(open: boolean) { function rarityMenuOpened(open: boolean) {
if (open) { if (open) {
@ -90,16 +99,24 @@ const CharacterSearchFilterBar = (props: Props) => {
} }
function sendFilters() { function sendFilters() {
const checkedRarityFilters = Object.values(rarityState).filter(x => x.checked).map((x, i) => x.id) const checkedRarityFilters = Object.values(rarityState)
const checkedElementFilters = Object.values(elementState).filter(x => x.checked).map((x, i) => x.id) .filter((x) => x.checked)
const checkedProficiency1Filters = Object.values(proficiency1State).filter(x => x.checked).map((x, i) => x.id) .map((x, i) => x.id)
const checkedProficiency2Filters = Object.values(proficiency2State).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 = { const filters = {
rarity: checkedRarityFilters, rarity: checkedRarityFilters,
element: checkedElementFilters, element: checkedElementFilters,
proficiency1: checkedProficiency1Filters, proficiency1: checkedProficiency1Filters,
proficiency2: checkedProficiency2Filters proficiency2: checkedProficiency2Filters,
} }
props.sendFilters(filters) props.sendFilters(filters)
@ -110,24 +127,35 @@ const CharacterSearchFilterBar = (props: Props) => {
}, [rarityState, elementState, proficiency1State, proficiency2State]) }, [rarityState, elementState, proficiency1State, proficiency2State])
function renderProficiencyFilter(proficiency: 1 | 2) { function renderProficiencyFilter(proficiency: 1 | 2) {
const onCheckedChange = (proficiency == 1) ? handleProficiency1Change : handleProficiency2Change const onCheckedChange =
const numSelected = (proficiency == 1) proficiency == 1 ? handleProficiency1Change : handleProficiency2Change
? Object.values(proficiency1State).map(x => x.checked).filter(Boolean).length const numSelected =
: Object.values(proficiency2State).map(x => x.checked).filter(Boolean).length proficiency == 1
const open = (proficiency == 1) ? proficiency1Menu : proficiency2Menu ? Object.values(proficiency1State)
const onOpenChange = (proficiency == 1) ? proficiency1MenuOpened : proficiency2MenuOpened .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 ( return (
<SearchFilter <SearchFilter
label={`${t('filters.labels.proficiency')} ${proficiency}`} label={`${t('filters.labels.proficiency')} ${proficiency}`}
numSelected={numSelected} numSelected={numSelected}
open={open} open={open}
onOpenChange={onOpenChange}> onOpenChange={onOpenChange}
<DropdownMenu.Label className="Label">{`${t('filters.labels.proficiency')} ${proficiency}`}</DropdownMenu.Label> >
<DropdownMenu.Label className="Label">{`${t(
'filters.labels.proficiency'
)} ${proficiency}`}</DropdownMenu.Label>
<section> <section>
<DropdownMenu.Group className="Group"> <DropdownMenu.Group className="Group">
{ Array.from(Array(proficiencies.length / 2)).map((x, i) => { {Array.from(Array(proficiencies.length / 2)).map((x, i) => {
const checked = (proficiency == 1) const checked =
proficiency == 1
? proficiency1State[proficiencies[i]].checked ? proficiency1State[proficiencies[i]].checked
: proficiency2State[proficiencies[i]].checked : proficiency2State[proficiencies[i]].checked
@ -136,28 +164,39 @@ const CharacterSearchFilterBar = (props: Props) => {
key={proficiencies[i]} key={proficiencies[i]}
onCheckedChange={onCheckedChange} onCheckedChange={onCheckedChange}
checked={checked} checked={checked}
valueKey={proficiencies[i]}> valueKey={proficiencies[i]}
>
{t(`proficiencies.${proficiencies[i]}`)} {t(`proficiencies.${proficiencies[i]}`)}
</SearchFilterCheckboxItem> </SearchFilterCheckboxItem>
)} )
) } })}
</DropdownMenu.Group> </DropdownMenu.Group>
<DropdownMenu.Group className="Group"> <DropdownMenu.Group className="Group">
{ Array.from(Array(proficiencies.length / 2)).map((x, i) => { {Array.from(Array(proficiencies.length / 2)).map((x, i) => {
const checked = (proficiency == 1) const checked =
? proficiency1State[proficiencies[i + (proficiencies.length / 2)]].checked proficiency == 1
: proficiency2State[proficiencies[i + (proficiencies.length / 2)]].checked ? proficiency1State[
proficiencies[i + proficiencies.length / 2]
].checked
: proficiency2State[
proficiencies[i + proficiencies.length / 2]
].checked
return ( return (
<SearchFilterCheckboxItem <SearchFilterCheckboxItem
key={proficiencies[i + (proficiencies.length / 2)]} key={proficiencies[i + proficiencies.length / 2]}
onCheckedChange={onCheckedChange} onCheckedChange={onCheckedChange}
checked={checked} checked={checked}
valueKey={proficiencies[i + (proficiencies.length / 2)]}> valueKey={proficiencies[i + proficiencies.length / 2]}
{t(`proficiencies.${proficiencies[i + (proficiencies.length / 2)]}`)} >
</SearchFilterCheckboxItem> {t(
`proficiencies.${
proficiencies[i + proficiencies.length / 2]
}`
)} )}
) } </SearchFilterCheckboxItem>
)
})}
</DropdownMenu.Group> </DropdownMenu.Group>
</section> </section>
</SearchFilter> </SearchFilter>
@ -166,38 +205,62 @@ const CharacterSearchFilterBar = (props: Props) => {
return ( return (
<div className="SearchFilterBar"> <div className="SearchFilterBar">
<SearchFilter label={t('filters.labels.rarity')} numSelected={Object.values(rarityState).map(x => x.checked).filter(Boolean).length} open={rarityMenu} onOpenChange={rarityMenuOpened}> <SearchFilter
<DropdownMenu.Label className="Label">{t('filters.labels.rarity')}</DropdownMenu.Label> label={t('filters.labels.rarity')}
{ Array.from(Array(rarities.length)).map((x, i) => { 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 ( return (
<SearchFilterCheckboxItem <SearchFilterCheckboxItem
key={rarities[i]} key={rarities[i]}
onCheckedChange={handleRarityChange} onCheckedChange={handleRarityChange}
checked={rarityState[rarities[i]].checked} checked={rarityState[rarities[i]].checked}
valueKey={rarities[i]}> valueKey={rarities[i]}
>
{t(`rarities.${rarities[i]}`)} {t(`rarities.${rarities[i]}`)}
</SearchFilterCheckboxItem> </SearchFilterCheckboxItem>
)} )
) } })}
</SearchFilter> </SearchFilter>
<SearchFilter label={t('filters.labels.element')} numSelected={Object.values(elementState).map(x => x.checked).filter(Boolean).length} open={elementMenu} onOpenChange={elementMenuOpened}> <SearchFilter
<DropdownMenu.Label className="Label">{t('filters.labels.element')}</DropdownMenu.Label> label={t('filters.labels.element')}
{ Array.from(Array(elements.length)).map((x, i) => { 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 ( return (
<SearchFilterCheckboxItem <SearchFilterCheckboxItem
key={elements[i]} key={elements[i]}
onCheckedChange={handleElementChange} onCheckedChange={handleElementChange}
checked={elementState[elements[i]].checked} checked={elementState[elements[i]].checked}
valueKey={elements[i]}> valueKey={elements[i]}
>
{t(`elements.${elements[i]}`)} {t(`elements.${elements[i]}`)}
</SearchFilterCheckboxItem> </SearchFilterCheckboxItem>
)} )
) } })}
</SearchFilter> </SearchFilter>
{ renderProficiencyFilter(1) } {renderProficiencyFilter(1)}
{ renderProficiencyFilter(2) } {renderProficiencyFilter(2)}
</div> </div>
) )
} }

View file

@ -27,7 +27,7 @@
} }
h3 { h3 {
color: #333; color: var(--text-primary);
font-size: $font-regular; font-size: $font-regular;
font-weight: $normal; font-weight: $normal;
line-height: 1.1; line-height: 1.1;
@ -43,10 +43,9 @@
z-index: 2; z-index: 2;
} }
.CharacterImage { .CharacterImage {
aspect-ratio: 131 / 273; aspect-ratio: 131 / 273;
background: white; background: var(--card-bg);
border: 1px solid rgba(0, 0, 0, 0); border: 1px solid rgba(0, 0, 0, 0);
border-radius: $unit; border-radius: $unit;
display: flex; display: flex;
@ -62,7 +61,7 @@
} }
&:hover .icon svg { &:hover .icon svg {
color: $grey-40; fill: var(--icon-secondary-hover);
} }
.icon { .icon {
@ -72,7 +71,7 @@
z-index: 1; z-index: 1;
svg { svg {
fill: $grey-70; fill: var(--icon-secondary);
} }
} }
} }

View file

@ -1,19 +1,19 @@
import React, { useEffect, useState } from "react" import React, { useEffect, useState } from 'react'
import { useRouter } from "next/router" import { useRouter } from 'next/router'
import { useSnapshot } from "valtio" import { useSnapshot } from 'valtio'
import { useTranslation } from "next-i18next" import { useTranslation } from 'next-i18next'
import classnames from "classnames" import classnames from 'classnames'
import { appState } from "~utils/appState" import { appState } from '~utils/appState'
import CharacterHovercard from "~components/CharacterHovercard" import CharacterHovercard from '~components/CharacterHovercard'
import SearchModal from "~components/SearchModal" import SearchModal from '~components/SearchModal'
import UncapIndicator from "~components/UncapIndicator" import UncapIndicator from '~components/UncapIndicator'
import PlusIcon from "~public/icons/Add.svg" 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 { interface Props {
gridCharacter?: GridCharacter gridCharacter?: GridCharacter
@ -24,15 +24,15 @@ interface Props {
} }
const CharacterUnit = (props: Props) => { const CharacterUnit = (props: Props) => {
const { t } = useTranslation("common") const { t } = useTranslation('common')
const { party, grid } = useSnapshot(appState) const { party, grid } = useSnapshot(appState)
const router = useRouter() const router = useRouter()
const locale = 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({ const classes = classnames({
CharacterUnit: true, CharacterUnit: true,
@ -48,19 +48,19 @@ const CharacterUnit = (props: Props) => {
}) })
function generateImageUrl() { function generateImageUrl() {
let imgSrc = "" let imgSrc = ''
if (props.gridCharacter) { if (props.gridCharacter) {
const character = props.gridCharacter.object! const character = props.gridCharacter.object!
// Change the image based on the uncap level // Change the image based on the uncap level
let suffix = "01" let suffix = '01'
if (props.gridCharacter.uncap_level == 6) suffix = "04" if (props.gridCharacter.uncap_level == 6) suffix = '04'
else if (props.gridCharacter.uncap_level == 5) suffix = "03" else if (props.gridCharacter.uncap_level == 5) suffix = '03'
else if (props.gridCharacter.uncap_level > 2) suffix = "02" else if (props.gridCharacter.uncap_level > 2) suffix = '02'
// Special casing for Lyria (and Young Cat eventually) // 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 let element = 1
if (grid.weapons.mainWeapon && grid.weapons.mainWeapon.element) { if (grid.weapons.mainWeapon && grid.weapons.mainWeapon.element) {
element = grid.weapons.mainWeapon.element element = grid.weapons.mainWeapon.element
@ -90,14 +90,14 @@ const CharacterUnit = (props: Props) => {
<PlusIcon /> <PlusIcon />
</span> </span>
) : ( ) : (
"" ''
)} )}
</div> </div>
) )
const editableImage = ( const editableImage = (
<SearchModal <SearchModal
placeholderText={t("search.placeholders.character")} placeholderText={t('search.placeholders.character')}
fromPosition={props.position} fromPosition={props.position}
object="characters" object="characters"
send={props.updateObject} send={props.updateObject}
@ -119,7 +119,7 @@ const CharacterUnit = (props: Props) => {
special={character.special} special={character.special}
/> />
) : ( ) : (
"" ''
)} )}
<h3 className="CharacterName">{character?.name[locale]}</h3> <h3 className="CharacterName">{character?.name[locale]}</h3>
</div> </div>

View 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%;
}
}

View 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

View file

@ -9,10 +9,10 @@
padding: calc($unit / 2); padding: calc($unit / 2);
.ToggleItem { .ToggleItem {
background: white; background: $grey-100;
border: none; border: none;
border-radius: 18px; border-radius: 18px;
color: $grey-40; color: $grey-50;
flex-grow: 1; flex-grow: 1;
font-size: $font-regular; font-size: $font-regular;
padding: ($unit) $unit * 2; padding: ($unit) $unit * 2;
@ -26,38 +26,39 @@
cursor: pointer; cursor: pointer;
} }
&:hover, &[data-state="on"] { &:hover,
background:$grey-80; &[data-state='on'] {
color: $grey-00; background: $grey-80;
color: $grey-15;
&.fire { &.fire {
background: $fire-bg-light; background: $fire-bg-20;
color: $fire-text-dark; color: $fire-text-10;
} }
&.water { &.water {
background: $water-bg-light; background: $water-bg-20;
color: $water-text-dark; color: $water-text-10;
} }
&.earth { &.earth {
background: $earth-bg-light; background: $earth-bg-20;
color: $earth-text-dark; color: $earth-text-10;
} }
&.wind { &.wind {
background: $wind-bg-light; background: $wind-bg-20;
color: $wind-text-dark; color: $wind-text-10;
} }
&.dark { &.dark {
background: $dark-bg-light; background: $dark-bg-10;
color: $dark-text-dark; color: $dark-text-10;
} }
&.light { &.light {
background: $light-bg-light; background: $light-bg-20;
color: $light-text-dark; color: $light-text-10;
} }
} }
} }

View file

@ -14,29 +14,64 @@ interface Props {
const ElementToggle = (props: Props) => { const ElementToggle = (props: Props) => {
const router = useRouter() const router = useRouter()
const { t } = useTranslation('common') const { t } = useTranslation('common')
const locale = (router.locale && ['en', 'ja'].includes(router.locale)) ? router.locale : 'en' const locale =
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
return ( return (
<ToggleGroup.Root className="ToggleGroup" type="single" defaultValue={`${props.currentElement}`} aria-label="Element" onValueChange={props.sendValue}> <ToggleGroup.Root
<ToggleGroup.Item className={`ToggleItem ${locale}`} value="0" aria-label="null"> 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')} {t('elements.null')}
</ToggleGroup.Item> </ToggleGroup.Item>
<ToggleGroup.Item className={`ToggleItem wind ${locale}`} value="1" aria-label="wind"> <ToggleGroup.Item
className={`ToggleItem wind ${locale}`}
value="1"
aria-label="wind"
>
{t('elements.wind')} {t('elements.wind')}
</ToggleGroup.Item> </ToggleGroup.Item>
<ToggleGroup.Item className={`ToggleItem fire ${locale}`} value="2" aria-label="fire"> <ToggleGroup.Item
className={`ToggleItem fire ${locale}`}
value="2"
aria-label="fire"
>
{t('elements.fire')} {t('elements.fire')}
</ToggleGroup.Item> </ToggleGroup.Item>
<ToggleGroup.Item className={`ToggleItem water ${locale}`} value="3" aria-label="water"> <ToggleGroup.Item
className={`ToggleItem water ${locale}`}
value="3"
aria-label="water"
>
{t('elements.water')} {t('elements.water')}
</ToggleGroup.Item> </ToggleGroup.Item>
<ToggleGroup.Item className={`ToggleItem earth ${locale}`} value="4" aria-label="earth"> <ToggleGroup.Item
className={`ToggleItem earth ${locale}`}
value="4"
aria-label="earth"
>
{t('elements.earth')} {t('elements.earth')}
</ToggleGroup.Item> </ToggleGroup.Item>
<ToggleGroup.Item className={`ToggleItem dark ${locale}`} value="5" aria-label="dark"> <ToggleGroup.Item
className={`ToggleItem dark ${locale}`}
value="5"
aria-label="dark"
>
{t('elements.dark')} {t('elements.dark')}
</ToggleGroup.Item> </ToggleGroup.Item>
<ToggleGroup.Item className={`ToggleItem light ${locale}`} value="6" aria-label="light"> <ToggleGroup.Item
className={`ToggleItem light ${locale}`}
value="6"
aria-label="light"
>
{t('elements.light')} {t('elements.light')}
</ToggleGroup.Item> </ToggleGroup.Item>
</ToggleGroup.Root> </ToggleGroup.Root>

View file

@ -1,5 +1,5 @@
#ExtraSummons { #ExtraSummons {
background: #FFEBD9; background: var(--subaura-orange-bg);
border-radius: 8px; border-radius: 8px;
box-sizing: border-box; box-sizing: border-box;
display: flex; display: flex;
@ -17,7 +17,7 @@
} }
& > span { & > span {
color: #825B39; color: var(--subaura-orange-text);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@ -46,10 +46,10 @@
} }
.SummonUnit .SummonImage { .SummonUnit .SummonImage {
background: #facea7; background: var(--subaura-orange-card-bg);
} }
.SummonUnit .SummonImage .icon svg { .SummonUnit .SummonImage .icon svg {
fill: #a8703f; fill: var(--subaura-orange-secondary);
} }
} }

View file

@ -1,8 +1,8 @@
import React from "react" import React from 'react'
import { useTranslation } from "next-i18next" import { useTranslation } from 'next-i18next'
import SummonUnit from "~components/SummonUnit" import SummonUnit from '~components/SummonUnit'
import { SearchableObject } from "~types" import { SearchableObject } from '~types'
import "./index.scss" import './index.scss'
// Props // Props
interface Props { interface Props {
@ -18,11 +18,11 @@ interface Props {
const ExtraSummons = (props: Props) => { const ExtraSummons = (props: Props) => {
const numSummons: number = 2 const numSummons: number = 2
const { t } = useTranslation("common") const { t } = useTranslation('common')
return ( return (
<div id="ExtraSummons"> <div id="ExtraSummons">
<span>{t("summons.subaura")}</span> <span>{t('summons.subaura')}</span>
<ul id="grid_summons"> <ul id="grid_summons">
{Array.from(Array(numSummons)).map((x, i) => { {Array.from(Array(numSummons)).map((x, i) => {
return ( return (

View file

@ -1,5 +1,5 @@
#ExtraGrid { #ExtraGrid {
background: #ECEBFF; background: var(--extra-purple-bg);
border-radius: 8px; border-radius: 8px;
box-sizing: border-box; box-sizing: border-box;
display: flex; display: flex;
@ -17,7 +17,7 @@
} }
& > span { & > span {
color: #4F3C79; color: var(--extra-purple-text);
display: flex; display: flex;
align-items: center; align-items: center;
flex-grow: 1; flex-grow: 1;
@ -38,10 +38,10 @@
} }
.WeaponUnit .WeaponImage { .WeaponUnit .WeaponImage {
background: #D5D3F6; background: var(--extra-purple-card-bg);
} }
.WeaponUnit .WeaponImage .icon svg { .WeaponUnit .WeaponImage .icon svg {
fill: #8F8AC6; fill: var(--extra-purple-secondary);
} }
} }

View file

@ -1,10 +1,10 @@
import React from "react" import React from 'react'
import { useTranslation } from "next-i18next" import { useTranslation } from 'next-i18next'
import WeaponUnit from "~components/WeaponUnit" import WeaponUnit from '~components/WeaponUnit'
import type { SearchableObject } from "~types" import type { SearchableObject } from '~types'
import "./index.scss" import './index.scss'
// Props // Props
interface Props { interface Props {
@ -18,17 +18,17 @@ interface Props {
const ExtraWeapons = (props: Props) => { const ExtraWeapons = (props: Props) => {
const numWeapons: number = 3 const numWeapons: number = 3
const { t } = useTranslation("common") const { t } = useTranslation('common')
return ( return (
<div id="ExtraGrid"> <div id="ExtraGrid">
<span>{t("extra_weapons")}</span> <span>{t('extra_weapons')}</span>
<ul className="grid_weapons"> <ul className="grid_weapons">
{Array.from(Array(numWeapons)).map((x, i) => { {Array.from(Array(numWeapons)).map((x, i) => {
return ( return (
<li key={`grid_unit_${i}`}> <li key={`grid_unit_${i}`}>
<WeaponUnit <WeaponUnit
editable={props.editable} editable={i < 2 ? props.editable : false}
position={props.offset + i} position={props.offset + i}
unitType={1} unitType={1}
gridWeapon={props.grid[props.offset + i]} gridWeapon={props.grid[props.offset + i]}

View file

@ -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 */
}

View file

@ -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

View file

@ -1,6 +1,6 @@
.FilterBar { .FilterBar {
align-items: center; align-items: center;
background: white; background: var(--bar-bg);
border-radius: 6px; border-radius: 6px;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@ -18,25 +18,38 @@
} }
h1 { h1 {
color: $grey-20; color: var(--text-primary);
font-size: $font-regular; font-size: $font-regular;
font-weight: $normal; font-weight: $normal;
flex-grow: 1; flex-grow: 1;
text-align: left; text-align: left;
} }
select { select,
background: url('/icons/Arrow.svg'), $grey-90; .SelectTrigger {
background-repeat: no-repeat; // background: url("/icons/Arrow.svg"), $grey-90;
background-position-y: center; // background-repeat: no-repeat;
background-position-x: 95%; // background-position-y: center;
background-size: $unit * 1.5; // background-position-x: 95%;
color: $grey-50; // background-size: $unit * 1.5;
background-color: var(--select-contained-bg);
color: $grey-55;
font-size: $font-small; font-size: $font-small;
margin: 0; margin: 0;
max-width: 200px; max-width: 200px;
&:hover {
background-color: var(--select-contained-bg-hover);
}
} }
.SelectTrigger {
width: 100%;
span {
font-size: $font-small;
}
}
.UserInfo { .UserInfo {
align-items: center; align-items: center;
@ -52,11 +65,11 @@
width: $diameter; width: $diameter;
&.gran { &.gran {
background-color: #CEE7FE; background-color: #cee7fe;
} }
&.djeeta { &.djeeta {
background-color: #FFE1FE; background-color: #ffe1fe;
} }
} }
} }

View file

@ -1,10 +1,12 @@
import React from 'react' import React, { useState } from 'react'
import { useTranslation } from 'next-i18next' import { useTranslation } from 'next-i18next'
import classNames from 'classnames' import classNames from 'classnames'
import RaidDropdown from '~components/RaidDropdown' import RaidDropdown from '~components/RaidDropdown'
import './index.scss' import './index.scss'
import Select from '~components/Select'
import SelectItem from '~components/SelectItem'
interface Props { interface Props {
children: React.ReactNode children: React.ReactNode
@ -12,13 +14,24 @@ interface Props {
element?: number element?: number
raidSlug?: string raidSlug?: string
recency?: number recency?: number
onFilter: ({element, raidSlug, recency} : { element?: number, raidSlug?: string, recency?: number}) => void onFilter: ({
element,
raidSlug,
recency,
}: {
element?: number
raidSlug?: string
recency?: number
}) => void
} }
const FilterBar = (props: Props) => { const FilterBar = (props: Props) => {
// Set up translation // Set up translation
const { t } = useTranslation('common') const { t } = useTranslation('common')
const [recencyOpen, setRecencyOpen] = useState(false)
const [elementOpen, setElementOpen] = useState(false)
// Set up refs for filter dropdowns // Set up refs for filter dropdowns
const elementSelect = React.createRef<HTMLSelectElement>() const elementSelect = React.createRef<HTMLSelectElement>()
const raidSelect = React.createRef<HTMLSelectElement>() const raidSelect = React.createRef<HTMLSelectElement>()
@ -26,17 +39,25 @@ const FilterBar = (props: Props) => {
// Set up classes object for showing shadow on scroll // Set up classes object for showing shadow on scroll
const classes = classNames({ const classes = classNames({
'FilterBar': true, FilterBar: true,
'shadow': props.scrolled shadow: props.scrolled,
}) })
function elementSelectChanged() { function openElementSelect() {
const elementValue = (elementSelect.current) ? parseInt(elementSelect.current.value) : -1 setElementOpen(!elementOpen)
}
function openRecencySelect() {
setRecencyOpen(!recencyOpen)
}
function elementSelectChanged(value: string) {
const elementValue = parseInt(value)
props.onFilter({ element: elementValue }) props.onFilter({ element: elementValue })
} }
function recencySelectChanged() { function recencySelectChanged(value: string) {
const recencyValue = (recencySelect.current) ? parseInt(recencySelect.current.value) : -1 const recencyValue = parseInt(value)
props.onFilter({ recency: recencyValue }) props.onFilter({ recency: recencyValue })
} }
@ -47,31 +68,76 @@ const FilterBar = (props: Props) => {
return ( return (
<div className={classes}> <div className={classes}>
{props.children} {props.children}
<select onChange={elementSelectChanged} ref={elementSelect} value={props.element}> <Select
<option data-element="all" key={-1} value={-1}>{t('elements.full.all')}</option> defaultValue={-1}
<option data-element="null" key={0} value={0}>{t('elements.full.null')}</option> trigger={'All elements'}
<option data-element="wind" key={1} value={1}>{t('elements.full.wind')}</option> open={elementOpen}
<option data-element="fire" key={2} value={2}>{t('elements.full.fire')}</option> onChange={elementSelectChanged}
<option data-element="water" key={3} value={3}>{t('elements.full.water')}</option> onClick={openElementSelect}
<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> <SelectItem data-element="all" key={-1} value={-1}>
<option data-element="light" key={6} value={6}>{t('elements.full.light')}</option> {t('elements.full.all')}
</select> </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 <RaidDropdown
currentRaid={props.raidSlug} currentRaid={props.raidSlug}
defaultRaid="all"
showAllRaidsOption={true} showAllRaidsOption={true}
onChange={raidSelectChanged} onChange={raidSelectChanged}
ref={raidSelect} ref={raidSelect}
/> />
<select onChange={recencySelectChanged} ref={recencySelect}>
<option key={-1} value={-1}>{t('recency.all_time')}</option> <Select
<option key={86400} value={86400}>{t('recency.last_day')}</option> defaultValue={-1}
<option key={604800} value={604800}>{t('recency.last_week')}</option> trigger={'All time'}
<option key={2629746} value={2629746}>{t('recency.last_month')}</option> open={recencyOpen}
<option key={7889238} value={7889238}>{t('recency.last_3_months')}</option> onChange={recencySelectChanged}
<option key={15778476} value={15778476}>{t('recency.last_6_months')}</option> onClick={openRecencySelect}
<option key={31556952} value={31556952}>{t('recency.last_year')}</option> >
</select> <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> </div>
) )
} }

View file

@ -6,14 +6,15 @@
padding: $unit * 2; padding: $unit * 2;
&:hover { &:hover {
background: white; background: var(--grid-rep-hover);
h2, .Grid { h2,
.Grid {
cursor: pointer; cursor: pointer;
} }
.Grid .weapon { .Grid .weapon {
box-shadow: inset 0 0 0 1px $grey-80; box-shadow: inset 0 0 0 1px var(--grid-border-color);
} }
} }
@ -23,7 +24,7 @@
flex-shrink: 0; flex-shrink: 0;
.weapon { .weapon {
background: white; background: var(--card-bg);
border-radius: 4px; border-radius: 4px;
} }
@ -49,8 +50,8 @@
width: 70px; width: 70px;
} }
.grid_mainhand img[src*="jpg"], .grid_mainhand img[src*='jpg'],
.grid_weapon img[src*="jpg"] { .grid_weapon img[src*='jpg'] {
border-radius: 4px; border-radius: 4px;
width: 100%; width: 100%;
height: 100%; height: 100%;
@ -63,7 +64,7 @@
gap: calc($unit / 2); gap: calc($unit / 2);
h2 { h2 {
color: $grey-00; color: var(--text-primary);
font-size: $font-regular; font-size: $font-regular;
overflow: hidden; overflow: hidden;
padding-bottom: 1px; padding-bottom: 1px;
@ -72,7 +73,7 @@
max-width: 258px; // Can we not do this? max-width: 258px; // Can we not do this?
&.empty { &.empty {
color: $grey-50; color: var(--text-tertiary);
} }
} }
@ -93,11 +94,6 @@
width: 14px; width: 14px;
height: 14px; height: 14px;
} }
button:hover,
button.Active {
background: $grey-90;
}
} }
.bottom { .bottom {
@ -105,12 +101,15 @@
flex-direction: row; flex-direction: row;
} }
.raid, .user, time { .raid,
color: $grey-50; .user,
time {
color: $grey-55;
font-size: $font-small; font-size: $font-small;
} }
.raid, .user { .raid,
.user {
flex-grow: 1; flex-grow: 1;
} }
@ -123,8 +122,8 @@
gap: calc($unit / 2); gap: calc($unit / 2);
align-items: center; align-items: center;
img,
img, .no-user { .no-user {
$diameter: 18px; $diameter: 18px;
border-radius: calc($diameter / 2); border-radius: calc($diameter / 2);
@ -133,11 +132,11 @@
} }
img.gran { img.gran {
background-color: #CEE7FE; background-color: #cee7fe;
} }
img.djeeta { img.djeeta {
background-color: #FFE1FE; background-color: #ffe1fe;
} }
.no-user { .no-user {

View file

@ -1,16 +1,17 @@
import React, { useEffect, useState } from "react" import React, { useEffect, useState } from 'react'
import { useRouter } from "next/router" import { useRouter } from 'next/router'
import { useSnapshot } from "valtio" import { useSnapshot } from 'valtio'
import { useTranslation } from "next-i18next" import { useTranslation } from 'next-i18next'
import classNames from "classnames" import classNames from 'classnames'
import { accountState } from "~utils/accountState" import { accountState } from '~utils/accountState'
import { formatTimeAgo } from "~utils/timeAgo" import { formatTimeAgo } from '~utils/timeAgo'
import Button from "~components/Button" import Button from '~components/Button'
import { ButtonType } from "~utils/enums"
import "./index.scss" import SaveIcon from '~public/icons/Save.svg'
import './index.scss'
interface Props { interface Props {
shortcode: string shortcode: string
@ -32,9 +33,9 @@ const GridRep = (props: Props) => {
const { account } = useSnapshot(accountState) const { account } = useSnapshot(accountState)
const router = useRouter() const router = useRouter()
const { t } = useTranslation("common") const { t } = useTranslation('common')
const locale = 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 [mainhand, setMainhand] = useState<Weapon>()
const [weapons, setWeapons] = useState<GridArray<Weapon>>({}) const [weapons, setWeapons] = useState<GridArray<Weapon>>({})
@ -75,7 +76,7 @@ const GridRep = (props: Props) => {
} }
function generateMainhandImage() { function generateMainhandImage() {
let url = "" let url = ''
if (mainhand) { if (mainhand) {
if (mainhand.element == 0 && props.grid[0].element) { if (mainhand.element == 0 && props.grid[0].element) {
@ -88,12 +89,12 @@ const GridRep = (props: Props) => {
return mainhand && props.grid[0] ? ( return mainhand && props.grid[0] ? (
<img alt={mainhand.name[locale]} src={url} /> <img alt={mainhand.name[locale]} src={url} />
) : ( ) : (
"" ''
) )
} }
function generateGridImage(position: number) { function generateGridImage(position: number) {
let url = "" let url = ''
const weapon = weapons[position] const weapon = weapons[position]
const gridWeapon = grid[position] const gridWeapon = grid[position]
@ -109,7 +110,7 @@ const GridRep = (props: Props) => {
return weapons[position] ? ( return weapons[position] ? (
<img alt={weapons[position]?.name[locale]} src={url} /> <img alt={weapons[position]?.name[locale]} src={url} />
) : ( ) : (
"" ''
) )
} }
@ -134,11 +135,11 @@ const GridRep = (props: Props) => {
const details = ( const details = (
<div className="Details"> <div className="Details">
<h2 className={titleClass} onClick={navigate}> <h2 className={titleClass} onClick={navigate}>
{props.name ? props.name : t("no_title")} {props.name ? props.name : t('no_title')}
</h2> </h2>
<div className="bottom"> <div className="bottom">
<div className={raidClass}> <div className={raidClass}>
{props.raid ? props.raid.name[locale] : t("no_raid")} {props.raid ? props.raid.name[locale] : t('no_raid')}
</div> </div>
<time className="last-updated" dateTime={props.createdAt.toISOString()}> <time className="last-updated" dateTime={props.createdAt.toISOString()}>
{formatTimeAgo(props.createdAt, locale)} {formatTimeAgo(props.createdAt, locale)}
@ -152,29 +153,31 @@ const GridRep = (props: Props) => {
<div className="top"> <div className="top">
<div className="info"> <div className="info">
<h2 className={titleClass} onClick={navigate}> <h2 className={titleClass} onClick={navigate}>
{props.name ? props.name : t("no_title")} {props.name ? props.name : t('no_title')}
</h2> </h2>
<div className={raidClass}> <div className={raidClass}>
{props.raid ? props.raid.name[locale] : t("no_raid")} {props.raid ? props.raid.name[locale] : t('no_raid')}
</div> </div>
</div> </div>
{account.authorized && {account.authorized &&
((props.user && account.user && account.user.id !== props.user.id) || ((props.user && account.user && account.user.id !== props.user.id) ||
!props.user) ? ( !props.user) ? (
<Button <Button
className="Save"
accessoryIcon={<SaveIcon class="stroke" />}
active={props.favorited} active={props.favorited}
icon="save" contained={true}
type={ButtonType.IconOnly} size="small"
onClick={sendSaveData} onClick={sendSaveData}
/> />
) : ( ) : (
"" ''
)} )}
</div> </div>
<div className="bottom"> <div className="bottom">
<div className={userClass}> <div className={userClass}>
{userImage()} {userImage()}
{props.user ? props.user.username : t("no_user")} {props.user ? props.user.username : t('no_user')}
</div> </div>
<time className="last-updated" dateTime={props.createdAt.toISOString()}> <time className="last-updated" dateTime={props.createdAt.toISOString()}>
{formatTimeAgo(props.createdAt, locale)} {formatTimeAgo(props.createdAt, locale)}

View file

@ -1,7 +1,7 @@
import classNames from "classnames" import classNames from 'classnames'
import React from "react" import React from 'react'
import "./index.scss" import './index.scss'
interface Props { interface Props {
children: React.ReactNode children: React.ReactNode

View file

@ -1,6 +1,6 @@
.Header { .Header {
display: flex; display: flex;
height: 34px; margin-bottom: $unit-2x;
width: 100%; width: 100%;
&.bottom { &.bottom {
@ -19,10 +19,10 @@
&:hover { &:hover {
padding-right: 50px; padding-right: 50px;
padding-bottom: 16px;
.Button { .Button {
background: white; background: var(--button-bg-hover);
color: var(--button-text-hover);
} }
.Menu { .Menu {

View file

@ -4,16 +4,16 @@ import './index.scss'
interface Props { interface Props {
position: 'top' | 'bottom' position: 'top' | 'bottom'
left: JSX.Element, left: JSX.Element
right: JSX.Element right: JSX.Element
} }
const Header = (props: Props) => { const Header = (props: Props) => {
return ( return (
<nav className={`Header ${props.position}`}> <nav className={`Header ${props.position}`}>
<div id="left">{ props.left }</div> <div id="left">{props.left}</div>
<div className="push" /> <div className="push" />
<div id="right">{ props.right }</div> <div id="right">{props.right}</div>
</nav> </nav>
) )
} }

View file

@ -1,24 +1,25 @@
.Menu { .Menu {
background: white; background: var(--menu-bg);
border-radius: 6px; border-radius: 6px;
display: none; display: none;
min-width: 220px; min-width: 220px;
position: absolute; position: absolute;
top: $unit * 5; // This shouldn't be hardcoded. How to calculate it? 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; z-index: 10;
} }
.MenuItem { .MenuItem {
color: $grey-40; color: var(--text-tertiary);
font-weight: $normal; font-weight: $normal;
&:hover:not(.disabled) { &:hover:not(.disabled) {
background: $grey-100; background: var(--menu-bg-item-hover);
color: $grey-00; color: var(--text-primary);
cursor: pointer; cursor: pointer;
a { a {
color: $grey-00; color: var(--text-primary);
} }
} }
@ -54,7 +55,7 @@
.Thumb { .Thumb {
$diameter: 18px; $diameter: 18px;
background: white; background: $grey-100;
border-radius: calc($diameter / 2); border-radius: calc($diameter / 2);
display: block; display: block;
height: $diameter; height: $diameter;
@ -67,14 +68,15 @@
cursor: pointer; cursor: pointer;
} }
&[data-state="checked"] { &[data-state='checked'] {
background: white; background: $grey-100;
transform: translateX(17px); transform: translateX(17px);
} }
} }
.left, .right { .left,
color: white; .right {
color: $grey-100;
font-size: 10px; font-size: 10px;
font-weight: $bold; font-weight: $bold;
position: absolute; position: absolute;
@ -94,10 +96,11 @@
} }
a { a {
color: $grey-40; color: $grey-50;
} }
& > a, & > span { & > a,
& > span {
display: block; display: block;
padding: 12px 12px; padding: 12px 12px;
} }
@ -110,8 +113,8 @@
&:hover { &:hover {
i.tag { i.tag {
background: $grey-60; background: var(--tag-bg);
color: white; color: var(--tag-text);
} }
} }
@ -129,7 +132,7 @@
} }
.MenuGroup { .MenuGroup {
border-bottom: 1px solid #f5f5f5; border-bottom: 1px solid var(--menu-separator);
&:first-child .MenuItem:first-child:hover { &:first-child .MenuItem:first-child:hover {
border-top-left-radius: 6px; border-top-left-radius: 6px;

View file

@ -1,17 +1,17 @@
import React, { useEffect, useState } from "react" import React, { useEffect, useState } from 'react'
import { getCookie, setCookie } from "cookies-next" import { getCookie, setCookie } from 'cookies-next'
import { useRouter } from "next/router" import { useRouter } from 'next/router'
import { useTranslation } from "next-i18next" import { useTranslation } from 'next-i18next'
import Link from "next/link" import Link from 'next/link'
import * as Switch from "@radix-ui/react-switch" import * as Switch from '@radix-ui/react-switch'
import AboutModal from "~components/AboutModal" import AboutModal from '~components/AboutModal'
import AccountModal from "~components/AccountModal" import AccountModal from '~components/AccountModal'
import LoginModal from "~components/LoginModal" import LoginModal from '~components/LoginModal'
import SignupModal from "~components/SignupModal" import SignupModal from '~components/SignupModal'
import "./index.scss" import './index.scss'
interface Props { interface Props {
authenticated: boolean authenticated: boolean
@ -21,30 +21,30 @@ interface Props {
const HeaderMenu = (props: Props) => { const HeaderMenu = (props: Props) => {
const router = useRouter() const router = useRouter()
const { t } = useTranslation("common") const { t } = useTranslation('common')
const accountCookie = getCookie("account") const accountCookie = getCookie('account')
const accountData: AccountCookie = accountCookie const accountData: AccountCookie = accountCookie
? JSON.parse(accountCookie as string) ? JSON.parse(accountCookie as string)
: null : null
const userCookie = getCookie("user") const userCookie = getCookie('user')
const userData: UserCookie = userCookie const userData: UserCookie = userCookie
? JSON.parse(userCookie as string) ? JSON.parse(userCookie as string)
: null : null
const localeCookie = getCookie("NEXT_LOCALE") const localeCookie = getCookie('NEXT_LOCALE')
const [checked, setChecked] = useState(false) const [checked, setChecked] = useState(false)
useEffect(() => { useEffect(() => {
const locale = localeCookie const locale = localeCookie
setChecked(locale === "ja" ? true : false) setChecked(locale === 'ja' ? true : false)
}, [localeCookie]) }, [localeCookie])
function handleCheckedChange(value: boolean) { function handleCheckedChange(value: boolean) {
const language = value ? "ja" : "en" const language = value ? 'ja' : 'en'
setCookie("NEXT_LOCALE", language, { path: "/" }) setCookie('NEXT_LOCALE', language, { path: '/' })
router.push(router.asPath, undefined, { locale: language }) router.push(router.asPath, undefined, { locale: language })
} }
@ -54,7 +54,7 @@ const HeaderMenu = (props: Props) => {
<ul className="Menu auth"> <ul className="Menu auth">
<div className="MenuGroup"> <div className="MenuGroup">
<li className="MenuItem profile"> <li className="MenuItem profile">
<Link href={`/${accountData.username}` || ""} passHref> <Link href={`/${accountData.username}` || ''} passHref>
<div> <div>
<span>{accountData.username}</span> <span>{accountData.username}</span>
<img <img
@ -68,18 +68,18 @@ const HeaderMenu = (props: Props) => {
</Link> </Link>
</li> </li>
<li className="MenuItem"> <li className="MenuItem">
<Link href={`/saved` || ""}>{t("menu.saved")}</Link> <Link href={`/saved` || ''}>{t('menu.saved')}</Link>
</li> </li>
</div> </div>
<div className="MenuGroup"> <div className="MenuGroup">
<li className="MenuItem"> <li className="MenuItem">
<Link href="/teams">{t("menu.teams")}</Link> <Link href="/teams">{t('menu.teams')}</Link>
</li> </li>
<li className="MenuItem disabled"> <li className="MenuItem disabled">
<div> <div>
<span>{t("menu.guides")}</span> <span>{t('menu.guides')}</span>
<i className="tag">{t("coming_soon")}</i> <i className="tag">{t('coming_soon')}</i>
</div> </div>
</li> </li>
</div> </div>
@ -87,7 +87,7 @@ const HeaderMenu = (props: Props) => {
<AboutModal /> <AboutModal />
<AccountModal /> <AccountModal />
<li className="MenuItem" onClick={props.logout}> <li className="MenuItem" onClick={props.logout}>
<span>{t("menu.logout")}</span> <span>{t('menu.logout')}</span>
</li> </li>
</div> </div>
</ul> </ul>
@ -100,7 +100,7 @@ const HeaderMenu = (props: Props) => {
<ul className="Menu unauth"> <ul className="Menu unauth">
<div className="MenuGroup"> <div className="MenuGroup">
<li className="MenuItem language"> <li className="MenuItem language">
<span>{t("menu.language")}</span> <span>{t('menu.language')}</span>
<Switch.Root <Switch.Root
className="Switch" className="Switch"
onCheckedChange={handleCheckedChange} onCheckedChange={handleCheckedChange}
@ -114,13 +114,13 @@ const HeaderMenu = (props: Props) => {
</div> </div>
<div className="MenuGroup"> <div className="MenuGroup">
<li className="MenuItem"> <li className="MenuItem">
<Link href="/teams">{t("menu.teams")}</Link> <Link href="/teams">{t('menu.teams')}</Link>
</li> </li>
<li className="MenuItem disabled"> <li className="MenuItem disabled">
<div> <div>
<span>{t("menu.guides")}</span> <span>{t('menu.guides')}</span>
<i className="tag">{t("coming_soon")}</i> <i className="tag">{t('coming_soon')}</i>
</div> </div>
</li> </li>
</div> </div>

View 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 */
}

View 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

View file

@ -0,0 +1,3 @@
.Job.SelectTrigger {
margin-bottom: $unit;
}

View file

@ -1,11 +1,15 @@
import React, { useEffect, useState } from "react" import React, { useEffect, useState } from 'react'
import { useRouter } from "next/router" import { useRouter } from 'next/router'
import { useSnapshot } from "valtio" import { useSnapshot } from 'valtio'
import { appState } from "~utils/appState" import Select from '~components/Select'
import { jobGroups } from "~utils/jobGroups" 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 // Props
interface Props { interface Props {
@ -20,12 +24,13 @@ const JobDropdown = React.forwardRef<HTMLSelectElement, Props>(
function useFieldSet(props, ref) { function useFieldSet(props, ref) {
// Set up router for locale // Set up router for locale
const router = useRouter() const router = useRouter()
const locale = router.locale || "en" const locale = router.locale || 'en'
// Create snapshot of app state // Create snapshot of app state
const { party } = useSnapshot(appState) const { party } = useSnapshot(appState)
// Set up local states for storing jobs // Set up local states for storing jobs
const [open, setOpen] = useState(false)
const [currentJob, setCurrentJob] = useState<Job>() const [currentJob, setCurrentJob] = useState<Job>()
const [jobs, setJobs] = useState<Job[]>() const [jobs, setJobs] = useState<Job[]>()
const [sortedJobs, setSortedJobs] = useState<GroupedJob>() const [sortedJobs, setSortedJobs] = useState<GroupedJob>()
@ -58,10 +63,14 @@ const JobDropdown = React.forwardRef<HTMLSelectElement, Props>(
} }
}, [appState, props.currentJob]) }, [appState, props.currentJob])
function openJobSelect() {
setOpen(!open)
}
// Enable changing select value // Enable changing select value
function handleChange(event: React.ChangeEvent<HTMLSelectElement>) { function handleChange(value: string) {
if (jobs) { 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) if (props.onChange) props.onChange(job)
setCurrentJob(job) setCurrentJob(job)
} }
@ -76,36 +85,37 @@ const JobDropdown = React.forwardRef<HTMLSelectElement, Props>(
.sort((a, b) => a.order - b.order) .sort((a, b) => a.order - b.order)
.map((item, i) => { .map((item, i) => {
return ( return (
<option key={i} value={item.id}> <SelectItem key={i} value={item.id}>
{item.name[locale]} {item.name[locale]}
</option> </SelectItem>
) )
}) })
const groupName = jobGroups.find((g) => g.slug === group)?.name[locale] const groupName = jobGroups.find((g) => g.slug === group)?.name[locale]
return ( return (
<optgroup key={group} label={groupName}> <SelectGroup key={group} label={groupName} separator={false}>
{options} {options}
</optgroup> </SelectGroup>
) )
} }
return ( return (
<select <Select
key={currentJob ? currentJob.id : -1} trigger={'Select a class...'}
value={currentJob ? currentJob.id : -1} placeholder={'Select a class...'}
onBlur={props.onBlur} open={open}
onClick={openJobSelect}
onChange={handleChange} onChange={handleChange}
ref={ref} triggerClass="Job"
> >
<option key="no-job" value={-1}> <SelectItem key={-1} value="no-job">
No class No class
</option> </SelectItem>
{sortedJobs {sortedJobs
? Object.keys(sortedJobs).map((x) => renderJobGroup(x)) ? Object.keys(sortedJobs).map((x) => renderJobGroup(x))
: ""} : ''}
</select> </Select>
) )
} }
) )

View file

@ -28,10 +28,10 @@
} }
.JobImage { .JobImage {
$height: 249px; $height: 252px;
$width: 447px; $width: 447px;
background: url("/images/background_a.jpg"); background: url('/images/background_a.jpg');
background-size: 500px 281px; background-size: 500px 281px;
border-radius: $unit; border-radius: $unit;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.2); box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.2);

View file

@ -1,17 +1,17 @@
import React, { ForwardedRef, useEffect, useState } from "react" import React, { ForwardedRef, useEffect, useState } from 'react'
import { useRouter } from "next/router" import { useRouter } from 'next/router'
import { useSnapshot } from "valtio" import { useSnapshot } from 'valtio'
import { useTranslation } from "next-i18next" import { useTranslation } from 'next-i18next'
import JobDropdown from "~components/JobDropdown" import JobDropdown from '~components/JobDropdown'
import JobSkillItem from "~components/JobSkillItem" import JobSkillItem from '~components/JobSkillItem'
import SearchModal from "~components/SearchModal" 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 // Props
interface Props { interface Props {
@ -24,14 +24,14 @@ interface Props {
const JobSection = (props: Props) => { const JobSection = (props: Props) => {
const { party } = useSnapshot(appState) const { party } = useSnapshot(appState)
const { t } = useTranslation("common") const { t } = useTranslation('common')
const router = useRouter() const router = useRouter()
const locale = 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 [job, setJob] = useState<Job>()
const [imageUrl, setImageUrl] = useState("") const [imageUrl, setImageUrl] = useState('')
const [numSkills, setNumSkills] = useState(4) const [numSkills, setNumSkills] = useState(4)
const [skills, setSkills] = useState<{ [key: number]: JobSkill | undefined }>( const [skills, setSkills] = useState<{ [key: number]: JobSkill | undefined }>(
[] []
@ -62,7 +62,7 @@ const JobSection = (props: Props) => {
if (job) { if (job) {
if ((party.job && job.id != party.job.id) || !party.job) if ((party.job && job.id != party.job.id) || !party.job)
appState.party.job = job appState.party.job = job
if (job.row === "1") setNumSkills(3) if (job.row === '1') setNumSkills(3)
else setNumSkills(4) else setNumSkills(4)
} }
}, [job]) }, [job])
@ -75,11 +75,11 @@ const JobSection = (props: Props) => {
} }
function generateImageUrl() { function generateImageUrl() {
let imgSrc = "" let imgSrc = ''
if (job) { if (job) {
const slug = job?.name.en.replaceAll(" ", "-").toLowerCase() const slug = job?.name.en.replaceAll(' ', '-').toLowerCase()
const gender = party.user && party.user.gender == 1 ? "b" : "a" const gender = party.user && party.user.gender == 1 ? 'b' : 'a'
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/jobs/${slug}_${gender}.png` imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/jobs/${slug}_${gender}.png`
} }
@ -101,7 +101,7 @@ const JobSection = (props: Props) => {
skill={skills[index]} skill={skills[index]}
editable={canEditSkill(skills[index])} editable={canEditSkill(skills[index])}
key={`skill-${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) => { const editableSkillItem = (index: number) => {
return ( return (
<SearchModal <SearchModal
placeholderText={t("search.placeholders.job_skill")} placeholderText={t('search.placeholders.job_skill')}
fromPosition={index} fromPosition={index}
object="job_skills" object="job_skills"
job={job} job={job}

View file

@ -15,13 +15,17 @@
} }
& p.placeholder { & p.placeholder {
color: $grey-20; color: var(--text-tertiary-hover);
}
& svg {
fill: var(--icon-tertiary-hover);
} }
} }
& > img, & > img,
& > div.placeholder { & > div.placeholder {
background: white; background: var(--card-bg);
border-radius: calc($unit / 2); border-radius: calc($unit / 2);
border: 1px solid rgba(0, 0, 0, 0); border: 1px solid rgba(0, 0, 0, 0);
width: $unit * 5; width: $unit * 5;
@ -34,13 +38,17 @@
justify-content: center; justify-content: center;
& > svg { & > svg {
fill: $grey-60; fill: var(--icon-secondary);
width: $unit * 2; width: $unit * 2;
height: $unit * 2; height: $unit * 2;
} }
} }
p.placeholder { p {
color: $grey-50; color: var(--text-primary);
&.placeholder {
color: var(--text-tertiary);
}
} }
} }

View file

@ -1,14 +1,14 @@
import React from "react" import React from 'react'
import { useRouter } from "next/router" import { useRouter } from 'next/router'
import { useTranslation } from "next-i18next" import { useTranslation } from 'next-i18next'
import classNames from "classnames" import classNames from 'classnames'
import PlusIcon from "~public/icons/Add.svg" import PlusIcon from '~public/icons/Add.svg'
import "./index.scss" import './index.scss'
// Props // Props
interface Props extends React.ComponentPropsWithoutRef<"div"> { interface Props extends React.ComponentPropsWithoutRef<'div'> {
skill?: JobSkill skill?: JobSkill
editable: boolean editable: boolean
hasJob: boolean hasJob: boolean
@ -17,11 +17,11 @@ interface Props extends React.ComponentPropsWithoutRef<"div"> {
const JobSkillItem = React.forwardRef<HTMLDivElement, Props>( const JobSkillItem = React.forwardRef<HTMLDivElement, Props>(
function useJobSkillItem({ ...props }, forwardedRef) { function useJobSkillItem({ ...props }, forwardedRef) {
const router = useRouter() const router = useRouter()
const { t } = useTranslation("common") const { t } = useTranslation('common')
const locale = const locale =
router.locale && ["en", "ja"].includes(router.locale) router.locale && ['en', 'ja'].includes(router.locale)
? router.locale ? router.locale
: "en" : 'en'
const classes = classNames({ const classes = classNames({
JobSkill: true, JobSkill: true,
@ -47,7 +47,7 @@ const JobSkillItem = React.forwardRef<HTMLDivElement, Props>(
} else { } else {
jsx = ( jsx = (
<div className={imageClasses}> <div className={imageClasses}>
{props.editable && props.hasJob ? <PlusIcon /> : ""} {props.editable && props.hasJob ? <PlusIcon /> : ''}
</div> </div>
) )
} }
@ -61,9 +61,9 @@ const JobSkillItem = React.forwardRef<HTMLDivElement, Props>(
if (props.skill) { if (props.skill) {
jsx = <p>{props.skill.name[locale]}</p> jsx = <p>{props.skill.name[locale]}</p>
} else if (props.editable && props.hasJob) { } 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 { } 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 return jsx

View file

@ -6,57 +6,57 @@
align-items: center; align-items: center;
&:hover { &:hover {
background: $grey-90; background: var(--button-contained-bg);
cursor: pointer; cursor: pointer;
.Info .skill.pill { .Info h5 {
background: $grey-80; color: var(--text-primary);
} }
} }
.Info { .Info {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
gap: calc($unit / 2); gap: $unit-half;
width: 100%; width: 100%;
.skill.pill { .skill.pill {
background: $grey-90; background: $grey-90;
border-radius: $unit * 2; border-radius: $unit * 2;
color: $grey-00; color: $grey-15;
display: inline; display: inline;
font-size: $font-tiny; font-size: $font-tiny;
font-weight: $medium; font-weight: $medium;
padding: calc($unit / 2) $unit; padding: $unit-half $unit;
&.buffing { &.buffing {
background-color: $light-bg-dark; background-color: $light-bg-10;
color: $light-text-dark; color: $light-text-10;
} }
&.debuffing { &.debuffing {
background-color: $water-bg-dark; background-color: $water-bg-10;
color: $water-text-dark; color: $water-text-10;
} }
&.healing { &.healing {
background-color: $wind-bg-dark; background-color: $wind-bg-10;
color: $wind-text-dark; color: $wind-text-10;
} }
&.damaging { &.damaging {
background-color: $fire-bg-dark; background-color: $fire-bg-10;
color: $fire-text-dark; color: $fire-text-10;
} }
&.field { &.field {
background-color: $dark-bg-dark; background-color: $dark-bg-20;
color: $dark-text-dark; color: $dark-text-10;
} }
} }
h5 { h5 {
color: #555; color: var(--text-secondary);
display: inline-block; display: inline-block;
font-size: $font-medium; font-size: $font-medium;
font-weight: $medium; font-weight: $medium;
@ -65,7 +65,7 @@
} }
img { img {
width: $unit * 6; width: $unit-6x;
height: $unit * 6; height: $unit-6x;
} }
} }

View file

@ -1,8 +1,8 @@
import React, { useEffect, useState } from "react" import React, { useEffect, useState } from 'react'
import { useRouter } from "next/router" import { useRouter } from 'next/router'
import { SkillGroup, skillClassification } from "~utils/skillGroups" import { SkillGroup, skillClassification } from '~utils/skillGroups'
import "./index.scss" import './index.scss'
interface Props { interface Props {
data: JobSkill data: JobSkill
@ -12,7 +12,7 @@ interface Props {
const JobSkillResult = (props: Props) => { const JobSkillResult = (props: Props) => {
const router = useRouter() const router = useRouter()
const locale = 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 const skill = props.data
@ -30,7 +30,7 @@ const JobSkillResult = (props: Props) => {
<img alt={skill.name[locale]} src={jobSkillUrl()} /> <img alt={skill.name[locale]} src={jobSkillUrl()} />
<div className="Info"> <div className="Info">
<h5>{skill.name[locale]}</h5> <h5>{skill.name[locale]}</h5>
<div className={`skill pill ${group?.name["en"].toLowerCase()}`}> <div className={`skill pill ${group?.name['en'].toLowerCase()}`}>
{group?.name[locale]} {group?.name[locale]}
</div> </div>
</div> </div>

View file

@ -1,3 +1,3 @@
.SearchFilterBar select { .SearchFilterBar .SelectTrigger {
background-color: $grey-90; width: 100%;
} }

View file

@ -1,10 +1,10 @@
import React, { useEffect, useState } from "react" import React, { useEffect, useState } from 'react'
import { useRouter } from "next/router" import { useTranslation } from 'react-i18next'
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 { interface Props {
sendFilters: (filters: { [key: string]: number }) => void sendFilters: (filters: { [key: string]: number }) => void
@ -12,15 +12,18 @@ interface Props {
const JobSkillSearchFilterBar = (props: Props) => { const JobSkillSearchFilterBar = (props: Props) => {
// Set up translation // Set up translation
const { t } = useTranslation("common") const { t } = useTranslation('common')
const [open, setOpen] = useState(false)
const [currentGroup, setCurrentGroup] = useState(-1) const [currentGroup, setCurrentGroup] = useState(-1)
function onChange(event: React.ChangeEvent<HTMLSelectElement>) { function openSelect() {
setCurrentGroup(parseInt(event.target.value)) setOpen(!open)
} }
function onBlur(event: React.ChangeEvent<HTMLSelectElement>) {} function onChange(value: string) {
setCurrentGroup(parseInt(value))
}
function sendFilters() { function sendFilters() {
const filters = { const filters = {
@ -36,34 +39,35 @@ const JobSkillSearchFilterBar = (props: Props) => {
return ( return (
<div className="SearchFilterBar"> <div className="SearchFilterBar">
<select <Select
key="job-skill-groups" defaultValue={-1}
value={currentGroup} trigger={'All elements'}
onBlur={onBlur} open={open}
onChange={onChange} onChange={onChange}
onClick={openSelect}
> >
<option key="all" value={-1}> <SelectItem key="all" value={-1}>
{t(`job_skills.all`)} {t(`job_skills.all`)}
</option> </SelectItem>
<option key="damaging" value={2}> <SelectItem key="damaging" value={2}>
{t(`job_skills.damaging`)} {t(`job_skills.damaging`)}
</option> </SelectItem>
<option key="buffing" value={0}> <SelectItem key="buffing" value={0}>
{t(`job_skills.buffing`)} {t(`job_skills.buffing`)}
</option> </SelectItem>
<option key="debuffing" value={1}> <SelectItem key="debuffing" value={1}>
{t(`job_skills.debuffing`)} {t(`job_skills.debuffing`)}
</option> </SelectItem>
<option key="healing" value={3}> <SelectItem key="healing" value={3}>
{t(`job_skills.healing`)} {t(`job_skills.healing`)}
</option> </SelectItem>
<option key="emp" value={4}> <SelectItem key="emp" value={4}>
{t(`job_skills.emp`)} {t(`job_skills.emp`)}
</option> </SelectItem>
<option key="base" value={5}> <SelectItem key="base" value={5}>
{t(`job_skills.base`)} {t(`job_skills.base`)}
</option> </SelectItem>
</select> </Select>
</div> </div>
) )
} }

View file

@ -5,7 +5,7 @@ interface Props {
children: ReactElement children: ReactElement
} }
const Layout = ({children}: Props) => { const Layout = ({ children }: Props) => {
return ( return (
<> <>
<TopHeader /> <TopHeader />

View file

@ -17,7 +17,7 @@
&:not(.btn-disabled) { &:not(.btn-disabled) {
background: $grey-90; background: $grey-90;
color: $grey-40; color: $grey-50;
&:hover { &:hover {
background: $grey-80; background: $grey-80;

View file

@ -1,19 +1,19 @@
import React, { useState } from "react" import React, { useState } from 'react'
import { setCookie } from "cookies-next" import { setCookie } from 'cookies-next'
import Router, { useRouter } from "next/router" import Router, { useRouter } from 'next/router'
import { useTranslation } from "react-i18next" import { useTranslation } from 'react-i18next'
import { AxiosResponse } from "axios" 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 api from '~utils/api'
import { accountState } from "~utils/accountState" import { accountState } from '~utils/accountState'
import Button from "~components/Button" import Button from '~components/Button'
import Fieldset from "~components/Fieldset" import Fieldset from '~components/Input'
import CrossIcon from "~public/icons/Cross.svg" import CrossIcon from '~public/icons/Cross.svg'
import "./index.scss" import './index.scss'
interface Props {} interface Props {}
@ -28,13 +28,13 @@ const emailRegex =
const LoginModal = (props: Props) => { const LoginModal = (props: Props) => {
const router = useRouter() const router = useRouter()
const { t } = useTranslation("common") const { t } = useTranslation('common')
// Set up form states and error handling // Set up form states and error handling
const [formValid, setFormValid] = useState(false) const [formValid, setFormValid] = useState(false)
const [errors, setErrors] = useState<ErrorMap>({ const [errors, setErrors] = useState<ErrorMap>({
email: "", email: '',
password: "", password: '',
}) })
// States // States
@ -50,17 +50,17 @@ const LoginModal = (props: Props) => {
let newErrors = { ...errors } let newErrors = { ...errors }
switch (name) { switch (name) {
case "email": case 'email':
if (value.length == 0) 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)) else if (!emailRegex.test(value))
newErrors.email = t("modals.login.errors.invalid_email") newErrors.email = t('modals.login.errors.invalid_email')
else newErrors.email = "" else newErrors.email = ''
break break
case "password": case 'password':
newErrors.password = newErrors.password =
value.length == 0 ? t("modals.login.errors.empty_password") : "" value.length == 0 ? t('modals.login.errors.empty_password') : ''
break break
default: default:
@ -91,7 +91,7 @@ const LoginModal = (props: Props) => {
const body = { const body = {
email: emailInput.current?.value, email: emailInput.current?.value,
password: passwordInput.current?.value, password: passwordInput.current?.value,
grant_type: "password", grant_type: 'password',
} }
if (formValid) { if (formValid) {
@ -119,7 +119,7 @@ const LoginModal = (props: Props) => {
token: response.data.access_token, token: response.data.access_token,
} }
setCookie("account", cookieObj, { path: "/" }) setCookie('account', cookieObj, { path: '/' })
} }
function storeUserInfo(response: AxiosResponse) { function storeUserInfo(response: AxiosResponse) {
@ -132,7 +132,7 @@ const LoginModal = (props: Props) => {
gender: user.gender, gender: user.gender,
} }
setCookie("user", cookieObj, { path: "/" }) setCookie('user', cookieObj, { path: '/' })
accountState.account.user = { accountState.account.user = {
id: user.id, id: user.id,
@ -142,7 +142,7 @@ const LoginModal = (props: Props) => {
gender: user.gender, gender: user.gender,
} }
console.log("Authorizing account...") console.log('Authorizing account...')
accountState.account.authorized = true accountState.account.authorized = true
setOpen(false) setOpen(false)
@ -151,7 +151,7 @@ const LoginModal = (props: Props) => {
function changeLanguage(newLanguage: string) { function changeLanguage(newLanguage: string) {
if (newLanguage !== router.locale) { if (newLanguage !== router.locale) {
setCookie("NEXT_LOCALE", newLanguage, { path: "/" }) setCookie('NEXT_LOCALE', newLanguage, { path: '/' })
router.push(router.asPath, undefined, { locale: newLanguage }) router.push(router.asPath, undefined, { locale: newLanguage })
} }
} }
@ -159,8 +159,8 @@ const LoginModal = (props: Props) => {
function openChange(open: boolean) { function openChange(open: boolean) {
setOpen(open) setOpen(open)
setErrors({ setErrors({
email: "", email: '',
password: "", password: '',
}) })
} }
@ -168,7 +168,7 @@ const LoginModal = (props: Props) => {
<Dialog.Root open={open} onOpenChange={openChange}> <Dialog.Root open={open} onOpenChange={openChange}>
<Dialog.Trigger asChild> <Dialog.Trigger asChild>
<li className="MenuItem"> <li className="MenuItem">
<span>{t("menu.login")}</span> <span>{t('menu.login')}</span>
</li> </li>
</Dialog.Trigger> </Dialog.Trigger>
<Dialog.Portal> <Dialog.Portal>
@ -178,7 +178,7 @@ const LoginModal = (props: Props) => {
> >
<div className="DialogHeader"> <div className="DialogHeader">
<Dialog.Title className="DialogTitle"> <Dialog.Title className="DialogTitle">
{t("modals.login.title")} {t('modals.login.title')}
</Dialog.Title> </Dialog.Title>
<Dialog.Close className="DialogClose" asChild> <Dialog.Close className="DialogClose" asChild>
<span> <span>
@ -190,7 +190,7 @@ const LoginModal = (props: Props) => {
<form className="form" onSubmit={login}> <form className="form" onSubmit={login}>
<Fieldset <Fieldset
fieldName="email" fieldName="email"
placeholder={t("modals.login.placeholders.email")} placeholder={t('modals.login.placeholders.email')}
onChange={handleChange} onChange={handleChange}
error={errors.email} error={errors.email}
ref={emailInput} ref={emailInput}
@ -198,13 +198,13 @@ const LoginModal = (props: Props) => {
<Fieldset <Fieldset
fieldName="password" fieldName="password"
placeholder={t("modals.login.placeholders.password")} placeholder={t('modals.login.placeholders.password')}
onChange={handleChange} onChange={handleChange}
error={errors.password} error={errors.password}
ref={passwordInput} ref={passwordInput}
/> />
<Button>{t("modals.login.buttons.confirm")}</Button> <Button>{t('modals.login.buttons.confirm')}</Button>
</form> </form>
</Dialog.Content> </Dialog.Content>
<Dialog.Overlay className="Overlay" /> <Dialog.Overlay className="Overlay" />

View file

@ -1,20 +1,20 @@
import React, { useCallback, useEffect, useMemo, useState } from "react" import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { useRouter } from "next/router" import { useRouter } from 'next/router'
import { useSnapshot } from "valtio" import { useSnapshot } from 'valtio'
import { getCookie } from "cookies-next" import { getCookie } from 'cookies-next'
import clonedeep from "lodash.clonedeep" import clonedeep from 'lodash.clonedeep'
import PartySegmentedControl from "~components/PartySegmentedControl" import PartySegmentedControl from '~components/PartySegmentedControl'
import PartyDetails from "~components/PartyDetails" import PartyDetails from '~components/PartyDetails'
import WeaponGrid from "~components/WeaponGrid" import WeaponGrid from '~components/WeaponGrid'
import SummonGrid from "~components/SummonGrid" import SummonGrid from '~components/SummonGrid'
import CharacterGrid from "~components/CharacterGrid" import CharacterGrid from '~components/CharacterGrid'
import api from "~utils/api" import api from '~utils/api'
import { appState, initialAppState } from "~utils/appState" import { appState, initialAppState } from '~utils/appState'
import { GridType, TeamElement } from "~utils/enums" import { GridType, TeamElement } from '~utils/enums'
import "./index.scss" import './index.scss'
// Props // Props
interface Props { interface Props {
@ -26,7 +26,7 @@ interface Props {
const Party = (props: Props) => { const Party = (props: Props) => {
// Cookies // Cookies
const cookie = getCookie("account") const cookie = getCookie('account')
const accountData: AccountCookie = cookie const accountData: AccountCookie = cookie
? JSON.parse(cookie as string) ? JSON.parse(cookie as string)
: null : null
@ -113,7 +113,7 @@ const Party = (props: Props) => {
.destroy({ id: appState.party.id, params: headers }) .destroy({ id: appState.party.id, params: headers })
.then(() => { .then(() => {
// Push to route // Push to route
router.push("/") router.push('/')
// Clean state // Clean state
const resetState = clonedeep(initialAppState) const resetState = clonedeep(initialAppState)
@ -188,16 +188,16 @@ const Party = (props: Props) => {
// Methods: Navigating with segmented control // Methods: Navigating with segmented control
function segmentClicked(event: React.ChangeEvent<HTMLInputElement>) { function segmentClicked(event: React.ChangeEvent<HTMLInputElement>) {
switch (event.target.value) { switch (event.target.value) {
case "class": case 'class':
setCurrentTab(GridType.Class) setCurrentTab(GridType.Class)
break break
case "characters": case 'characters':
setCurrentTab(GridType.Character) setCurrentTab(GridType.Character)
break break
case "weapons": case 'weapons':
setCurrentTab(GridType.Weapon) setCurrentTab(GridType.Weapon)
break break
case "summons": case 'summons':
setCurrentTab(GridType.Summon) setCurrentTab(GridType.Summon)
break break
default: default:
@ -253,7 +253,7 @@ const Party = (props: Props) => {
} }
return ( return (
<div> <React.Fragment>
{navigation} {navigation}
<section id="Party">{currentGrid()}</section> <section id="Party">{currentGrid()}</section>
{ {
@ -263,7 +263,7 @@ const Party = (props: Props) => {
deleteCallback={deleteTeam} deleteCallback={deleteTeam}
/> />
} }
</div> </React.Fragment>
) )
} }

View file

@ -1,23 +1,21 @@
.PartyDetails { .PartyDetails {
display: none; // This breaks transition, find a workaround display: none; // This breaks transition, find a workaround
opacity: 0; opacity: 0;
margin: 0 auto; margin: $unit-4x auto 0;
margin-bottom: 100px; max-width: $unit * 94;
max-width: $unit * 95;
position: relative; position: relative;
&.Editable { &.Editable {
top: $unit; top: $unit;
height: 0; height: 0;
z-index: 2; z-index: 2;
transition: opacity 0.2s ease-in-out, transition: opacity 0.2s ease-in-out, top 0.2s ease-in-out;
top 0.2s ease-in-out;
&.Visible { &.Visible {
display: block; display: flex;
flex-direction: column;
gap: $unit;
height: auto; height: auto;
margin-bottom: 40vh;
opacity: 1; opacity: 1;
top: 0; top: 0;
} }
@ -36,6 +34,7 @@
display: flex; display: flex;
flex-direction: row; flex-direction: row;
gap: $unit; gap: $unit;
margin-bottom: $unit-12x;
.left { .left {
flex-grow: 1; flex-grow: 1;
@ -51,8 +50,7 @@
&.ReadOnly { &.ReadOnly {
top: $unit * -1; top: $unit * -1;
transition: opacity 0.2s ease-in-out, transition: opacity 0.2s ease-in-out, top 0.2s ease-in-out;
top 0.2s ease-in-out;
&.Visible { &.Visible {
display: block; display: block;
@ -71,7 +69,6 @@
white-space: pre-line; white-space: pre-line;
} }
h1 { h1 {
font-size: $font-xlarge; font-size: $font-xlarge;
font-weight: $normal; font-weight: $normal;
@ -88,6 +85,14 @@
.left { .left {
flex-grow: 1; flex-grow: 1;
h1 {
color: var(--text-primary);
&.empty {
color: var(--text-secondary);
}
}
} }
} }
@ -108,7 +113,7 @@
} }
& > *:not(:last-child):after { & > *:not(:last-child):after {
content: " · "; content: ' · ';
margin: 0 calc($unit / 2); margin: 0 calc($unit / 2);
} }
} }
@ -119,7 +124,8 @@
gap: calc($unit / 2); gap: calc($unit / 2);
margin-top: 1px; margin-top: 1px;
img, .no-user { img,
.no-user {
$diameter: 24px; $diameter: 24px;
border-radius: calc($diameter / 2); border-radius: calc($diameter / 2);
@ -128,11 +134,11 @@
} }
img.gran { img.gran {
background-color: #CEE7FE; background-color: #cee7fe;
} }
img.djeeta { img.djeeta {
background-color: #FFE1FE; background-color: #ffe1fe;
} }
.no-user { .no-user {
@ -145,7 +151,7 @@
.EmptyDetails { .EmptyDetails {
display: none; display: none;
justify-content: center; justify-content: center;
margin-bottom: $unit * 10; margin: $unit-4x 0 $unit-10x;
&.Visible { &.Visible {
display: flex; display: flex;

View file

@ -1,34 +1,37 @@
import React, { useState } from "react" import React, { useState } from 'react'
import Head from "next/head" import Head from 'next/head'
import { useRouter } from "next/router" import { useRouter } from 'next/router'
import { useSnapshot } from "valtio" import { useSnapshot } from 'valtio'
import { useTranslation } from "next-i18next" import { useTranslation } from 'next-i18next'
import Linkify from "react-linkify" import Linkify from 'react-linkify'
import classNames from "classnames" import classNames from 'classnames'
import * as AlertDialog from "@radix-ui/react-alert-dialog" import * as AlertDialog from '@radix-ui/react-alert-dialog'
import CrossIcon from "~public/icons/Cross.svg"
import Button from "~components/Button" import Button from '~components/Button'
import CharLimitedFieldset from "~components/CharLimitedFieldset" import CharLimitedFieldset from '~components/CharLimitedFieldset'
import RaidDropdown from "~components/RaidDropdown" import RaidDropdown from '~components/RaidDropdown'
import TextFieldset from "~components/TextFieldset" import TextFieldset from '~components/TextFieldset'
import { accountState } from "~utils/accountState" import { accountState } from '~utils/accountState'
import { appState } from "~utils/appState" import { appState } from '~utils/appState'
import "./index.scss" import CheckIcon from '~public/icons/Check.svg'
import Link from "next/link" import CrossIcon from '~public/icons/Cross.svg'
import { formatTimeAgo } from "~utils/timeAgo" import EditIcon from '~public/icons/Edit.svg'
import './index.scss'
import Link from 'next/link'
import { formatTimeAgo } from '~utils/timeAgo'
const emptyRaid: Raid = { const emptyRaid: Raid = {
id: "", id: '',
name: { name: {
en: "", en: '',
ja: "", ja: '',
}, },
slug: "", slug: '',
level: 0, level: 0,
group: 0, group: 0,
element: 0, element: 0,
@ -47,9 +50,9 @@ const PartyDetails = (props: Props) => {
const { party, raids } = useSnapshot(appState) const { party, raids } = useSnapshot(appState)
const { account } = useSnapshot(accountState) const { account } = useSnapshot(accountState)
const { t } = useTranslation("common") const { t } = useTranslation('common')
const router = useRouter() const router = useRouter()
const locale = router.locale || "en" const locale = router.locale || 'en'
const nameInput = React.createRef<HTMLInputElement>() const nameInput = React.createRef<HTMLInputElement>()
const descriptionInput = React.createRef<HTMLTextAreaElement>() const descriptionInput = React.createRef<HTMLTextAreaElement>()
@ -87,8 +90,8 @@ const PartyDetails = (props: Props) => {
}) })
const [errors, setErrors] = useState<{ [key: string]: string }>({ const [errors, setErrors] = useState<{ [key: string]: string }>({
name: "", name: '',
description: "", description: '',
}) })
function handleInputChange(event: React.ChangeEvent<HTMLInputElement>) { function handleInputChange(event: React.ChangeEvent<HTMLInputElement>) {
@ -140,7 +143,7 @@ const PartyDetails = (props: Props) => {
return ( return (
<div className={userClass}> <div className={userClass}>
{userImage()} {userImage()}
{party.user ? party.user.username : t("no_user")} {party.user ? party.user.username : t('no_user')}
</div> </div>
) )
} }
@ -169,30 +172,30 @@ const PartyDetails = (props: Props) => {
if (party.editable) { if (party.editable) {
return ( return (
<AlertDialog.Root> <AlertDialog.Root>
<AlertDialog.Trigger className="Button destructive"> <AlertDialog.Trigger className="Button Blended medium destructive">
<span className="icon"> <span className="Accessory">
<CrossIcon /> <CrossIcon />
</span> </span>
<span className="text">{t("buttons.delete")}</span> <span className="Text">{t('buttons.delete')}</span>
</AlertDialog.Trigger> </AlertDialog.Trigger>
<AlertDialog.Portal> <AlertDialog.Portal>
<AlertDialog.Overlay className="Overlay" /> <AlertDialog.Overlay className="Overlay" />
<AlertDialog.Content className="Dialog"> <AlertDialog.Content className="Dialog">
<AlertDialog.Title className="DialogTitle"> <AlertDialog.Title className="DialogTitle">
{t("modals.delete_team.title")} {t('modals.delete_team.title')}
</AlertDialog.Title> </AlertDialog.Title>
<AlertDialog.Description className="DialogDescription"> <AlertDialog.Description className="DialogDescription">
{t("modals.delete_team.description")} {t('modals.delete_team.description')}
</AlertDialog.Description> </AlertDialog.Description>
<div className="actions"> <div className="actions">
<AlertDialog.Cancel className="Button modal"> <AlertDialog.Cancel className="Button modal">
{t("modals.delete_team.buttons.cancel")} {t('modals.delete_team.buttons.cancel')}
</AlertDialog.Cancel> </AlertDialog.Cancel>
<AlertDialog.Action <AlertDialog.Action
className="Button modal destructive" className="Button modal destructive"
onClick={(e) => props.deleteCallback(e)} onClick={(e) => props.deleteCallback(e)}
> >
{t("modals.delete_team.buttons.confirm")} {t('modals.delete_team.buttons.confirm')}
</AlertDialog.Action> </AlertDialog.Action>
</div> </div>
</AlertDialog.Content> </AlertDialog.Content>
@ -200,7 +203,7 @@ const PartyDetails = (props: Props) => {
</AlertDialog.Root> </AlertDialog.Root>
) )
} else { } else {
return "" return ''
} }
} }
@ -217,13 +220,13 @@ const PartyDetails = (props: Props) => {
/> />
<RaidDropdown <RaidDropdown
showAllRaidsOption={false} showAllRaidsOption={false}
currentRaid={party.raid?.slug || ""} currentRaid={party.raid?.slug || ''}
ref={raidSelect} ref={raidSelect}
/> />
<TextFieldset <TextFieldset
fieldName="name" fieldName="name"
placeholder={ placeholder={
"Write your notes here\n\n\nWatch out for the 50% trigger!\nMake sure to click Fediels 1 first\nGood luck with RNG!" 'Write your notes here\n\n\nWatch out for the 50% trigger!\nMake sure to click Fediels 1 first\nGood luck with RNG!'
} }
value={party.description} value={party.description}
onChange={handleTextAreaChange} onChange={handleTextAreaChange}
@ -233,16 +236,15 @@ const PartyDetails = (props: Props) => {
<div className="bottom"> <div className="bottom">
<div className="left"> <div className="left">
{router.pathname !== "/new" ? deleteButton() : ""} {router.pathname !== '/new' ? deleteButton() : ''}
</div> </div>
<div className="right"> <div className="right">
<Button active={true} onClick={toggleDetails}> <Button text={t('buttons.cancel')} onClick={toggleDetails} />
{t("buttons.cancel")} <Button
</Button> accessoryIcon={<CheckIcon className="Check" />}
text={t('buttons.save_info')}
<Button active={true} icon="check" onClick={updateDetails}> onClick={updateDetails}
{t("buttons.save_info")} />
</Button>
</div> </div>
</div> </div>
</section> </section>
@ -252,10 +254,12 @@ const PartyDetails = (props: Props) => {
<section className={readOnlyClasses}> <section className={readOnlyClasses}>
<div className="info"> <div className="info">
<div className="left"> <div className="left">
{party.name ? <h1>{party.name}</h1> : ""} <h1 className={!party.name ? 'empty' : ''}>
{party.name ? party.name : 'Untitled'}
</h1>
<div className="attribution"> <div className="attribution">
{party.user ? linkedUserBlock(party.user) : userBlock()} {party.user ? linkedUserBlock(party.user) : userBlock()}
{party.raid ? linkedRaidBlock(party.raid) : ""} {party.raid ? linkedRaidBlock(party.raid) : ''}
{party.created_at != undefined ? ( {party.created_at != undefined ? (
<time <time
className="last-updated" className="last-updated"
@ -264,15 +268,17 @@ const PartyDetails = (props: Props) => {
{formatTimeAgo(new Date(party.created_at), locale)} {formatTimeAgo(new Date(party.created_at), locale)}
</time> </time>
) : ( ) : (
"" ''
)} )}
</div> </div>
</div> </div>
<div className="right"> <div className="right">
{party.editable ? ( {party.editable ? (
<Button active={true} icon="edit" onClick={toggleDetails}> <Button
{t("buttons.show_info")} accessoryIcon={<EditIcon />}
</Button> text={t('buttons.show_info')}
onClick={toggleDetails}
/>
) : ( ) : (
<div /> <div />
)} )}
@ -283,7 +289,7 @@ const PartyDetails = (props: Props) => {
<Linkify>{party.description}</Linkify> <Linkify>{party.description}</Linkify>
</p> </p>
) : ( ) : (
"" ''
)} )}
</section> </section>
) )
@ -291,59 +297,24 @@ const PartyDetails = (props: Props) => {
const emptyDetails = ( const emptyDetails = (
<div className={emptyClasses}> <div className={emptyClasses}>
{party.editable ? ( {party.editable ? (
<Button active={true} icon="edit" onClick={toggleDetails}> <Button
{t("buttons.show_info")} accessoryIcon={<EditIcon />}
</Button> text={t('buttons.show_info')}
onClick={toggleDetails}
/>
) : ( ) : (
<div /> <div />
)} )}
</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 ( return (
<div> <React.Fragment>
<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>
{editable && (party.name || party.description || party.raid) {editable && (party.name || party.description || party.raid)
? readOnly ? readOnly
: emptyDetails} : emptyDetails}
{editable} {editable}
</div> </React.Fragment>
) )
} }

View file

@ -10,7 +10,6 @@ import ToggleSwitch from '~components/ToggleSwitch'
import { GridType } from '~utils/enums' import { GridType } from '~utils/enums'
import './index.scss' import './index.scss'
interface Props { interface Props {
@ -28,20 +27,31 @@ const PartySegmentedControl = (props: Props) => {
let element: number = 0 let element: number = 0
if (party.element == 0 && grid.weapons.mainWeapon) if (party.element == 0 && grid.weapons.mainWeapon)
element = grid.weapons.mainWeapon.element element = grid.weapons.mainWeapon.element
else else element = party.element
element = party.element
switch(element) { switch (element) {
case 1: return "wind"; break case 1:
case 2: return "fire"; break return 'wind'
case 3: return "water"; break break
case 4: return "earth"; break case 2:
case 5: return "dark"; break return 'fire'
case 6: return "light"; break break
case 3:
return 'water'
break
case 4:
return 'earth'
break
case 5:
return 'dark'
break
case 6:
return 'light'
break
} }
} }
const extraToggle = const extraToggle = (
<div className="ExtraSwitch"> <div className="ExtraSwitch">
Extra Extra
<ToggleSwitch <ToggleSwitch
@ -51,6 +61,7 @@ const PartySegmentedControl = (props: Props) => {
onChange={props.onCheckboxChange} onChange={props.onCheckboxChange}
/> />
</div> </div>
)
return ( return (
<div className="PartyNavigation"> <div className="PartyNavigation">
@ -67,30 +78,34 @@ const PartySegmentedControl = (props: Props) => {
name="characters" name="characters"
selected={props.selectedTab == GridType.Character} selected={props.selectedTab == GridType.Character}
onClick={props.onClick} onClick={props.onClick}
>{t('party.segmented_control.characters')}</Segment> >
{t('party.segmented_control.characters')}
</Segment>
<Segment <Segment
groupName="grid" groupName="grid"
name="weapons" name="weapons"
selected={props.selectedTab == GridType.Weapon} selected={props.selectedTab == GridType.Weapon}
onClick={props.onClick} onClick={props.onClick}
>{t('party.segmented_control.weapons')}</Segment> >
{t('party.segmented_control.weapons')}
</Segment>
<Segment <Segment
groupName="grid" groupName="grid"
name="summons" name="summons"
selected={props.selectedTab == GridType.Summon} selected={props.selectedTab == GridType.Summon}
onClick={props.onClick} onClick={props.onClick}
>{t('party.segmented_control.summons')}</Segment> >
{t('party.segmented_control.summons')}
</Segment>
</SegmentedControl> </SegmentedControl>
{ {(() => {
(() => {
if (party.editable && props.selectedTab == GridType.Weapon) { if (party.editable && props.selectedTab == GridType.Weapon) {
return extraToggle return extraToggle
} }
})() })()}
}
</div> </div>
) )
} }

View file

@ -1,6 +1,10 @@
import React, { useCallback, useEffect, useState } from 'react' import React, { useCallback, useEffect, useState } from 'react'
import { useRouter } from 'next/router' 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 api from '~utils/api'
import { appState } from '~utils/appState' import { appState } from '~utils/appState'
import { raidGroups } from '~utils/raidGroups' import { raidGroups } from '~utils/raidGroups'
@ -11,40 +15,51 @@ import './index.scss'
interface Props { interface Props {
showAllRaidsOption: boolean showAllRaidsOption: boolean
currentRaid?: string currentRaid?: string
defaultRaid?: string
onChange?: (slug?: string) => void onChange?: (slug?: string) => void
onBlur?: (event: React.ChangeEvent<HTMLSelectElement>) => 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 // Set up router for locale
const router = useRouter() const router = useRouter()
const locale = router.locale || 'en' const locale = router.locale || 'en'
// Set up local states for storing raids // Set up local states for storing raids
const [open, setOpen] = useState(false)
const [currentRaid, setCurrentRaid] = useState<Raid>() const [currentRaid, setCurrentRaid] = useState<Raid>()
const [raids, setRaids] = useState<Raid[]>() const [raids, setRaids] = useState<Raid[]>()
const [sortedRaids, setSortedRaids] = useState<Raid[][]>() const [sortedRaids, setSortedRaids] = useState<Raid[][]>()
function openRaidSelect() {
setOpen(!open)
}
// Organize raids into groups on mount // Organize raids into groups on mount
const organizeRaids = useCallback((raids: Raid[]) => { const organizeRaids = useCallback(
(raids: Raid[]) => {
// Set up empty raid for "All raids" // Set up empty raid for "All raids"
const all = { const all = {
id: '0', id: '0',
name: { name: {
en: 'All raids', en: 'All raids',
ja: '全て' ja: '全て',
}, },
slug: 'all', slug: 'all',
level: 0, level: 0,
group: 0, group: 0,
element: 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 = [] let groupedRaids = []
for (let i = 0; i <= numGroups; i++) { 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) { if (props.showAllRaidsOption) {
@ -55,58 +70,79 @@ const RaidDropdown = React.forwardRef<HTMLSelectElement, Props>(function useFiel
setRaids(raids) setRaids(raids)
setSortedRaids(groupedRaids) setSortedRaids(groupedRaids)
appState.raids = raids appState.raids = raids
}, [props.showAllRaidsOption]) },
[props.showAllRaidsOption]
)
// Fetch all raids on mount // Fetch all raids on mount
useEffect(() => { useEffect(() => {
api.endpoints.raids.getAll() api.endpoints.raids
.then(response => organizeRaids(response.data.map((r: any) => r.raid))) .getAll()
.then((response) =>
organizeRaids(response.data.map((r: any) => r.raid))
)
}, [organizeRaids]) }, [organizeRaids])
// Set current raid on mount // Set current raid on mount
useEffect(() => { useEffect(() => {
if (raids && props.currentRaid) { if (raids && props.currentRaid) {
const raid = raids.find(raid => raid.slug === props.currentRaid) const raid = raids.find((raid) => raid.slug === props.currentRaid)
setCurrentRaid(raid) setCurrentRaid(raid)
} }
}, [raids, props.currentRaid]) }, [raids, props.currentRaid])
// Enable changing select value // Enable changing select value
function handleChange(event: React.ChangeEvent<HTMLSelectElement>) { function handleChange(value: string) {
if (props.onChange) props.onChange(event.target.value) console.log(value)
if (props.onChange) props.onChange(value)
if (raids) { if (raids) {
const raid = raids.find(raid => raid.slug === event.target.value) const raid = raids.find((raid) => raid.slug === value)
setCurrentRaid(raid) setCurrentRaid(raid)
} }
} }
// Render JSX for each raid option, sorted into optgroups // Render JSX for each raid option, sorted into optgroups
function renderRaidGroup(index: number) { function renderRaidGroup(index: number) {
const options = sortedRaids && sortedRaids.length > 0 && sortedRaids[index].length > 0 && const options =
sortedRaids[index].sort((a, b) => a.element - b.element).map((item, i) => { sortedRaids &&
sortedRaids.length > 0 &&
sortedRaids[index].length > 0 &&
sortedRaids[index]
.sort((a, b) => a.element - b.element)
.map((item, i) => {
return ( return (
<option key={i} value={item.slug}>{item.name[locale]}</option> <SelectItem key={i} value={item.slug}>
{item.name[locale]}
</SelectItem>
) )
}) })
return ( return (
<optgroup key={index} label={raidGroups[index].name[locale]}> <SelectGroup
key={index}
label={raidGroups[index].name[locale]}
separator={false}
>
{options} {options}
</optgroup> </SelectGroup>
) )
} }
return ( return (
<select <Select
key={currentRaid?.slug} defaultValue={props.defaultRaid}
value={currentRaid?.slug} trigger={'Select a raid...'}
onBlur={props.onBlur} placeholder={'Select a raid...'}
open={open}
onClick={openRaidSelect}
onChange={handleChange} onChange={handleChange}
ref={ref}> >
{ Array.from(Array(sortedRaids?.length)).map((x, i) => renderRaidGroup(i)) } {Array.from(Array(sortedRaids?.length)).map((x, i) =>
</select> renderRaidGroup(i)
)}
</Select>
) )
}) }
)
export default RaidDropdown export default RaidDropdown

View file

@ -1,33 +1,32 @@
.DropdownLabel { button.DropdownLabel {
align-items: center; align-items: center;
background: $grey-90; background: var(--button-contained-bg);
border: none; border: none;
border-radius: $unit * 2; border-radius: $unit-2x;
color: $grey-40; color: var(--text-secondary);
display: flex; display: flex;
gap: calc($unit / 2); font-size: $font-small;
gap: $unit-half;
flex-direction: row; flex-direction: row;
padding: ($unit) ($unit * 2); padding: $unit ($unit * 1.5) $unit $unit-2x;
&:hover { &:hover {
background: $grey-80; background: var(--button-contained-bg-hover);
color: $grey-00; color: var(--text-primary);
cursor: pointer; cursor: pointer;
} }
.count { .count {
color: $grey-60; color: var(--text-tertiary);
font-weight: $medium; font-weight: $medium;
} }
& > .icon { & > .icon {
$diameter: 12px; $diameter: 16px;
height: $diameter; height: $diameter;
width: $diameter; width: $diameter;
svg { svg {
transform: scale(0.85);
path { path {
fill: $grey-60; fill: $grey-60;
} }
@ -36,12 +35,11 @@
} }
.Dropdown { .Dropdown {
background: white; background: var(--button-contained-bg);
border-radius: $unit; border-radius: $unit;
box-shadow: 0 0 2px rgba(0, 0, 0, 0.18); box-shadow: 0 0 2px rgba(0, 0, 0, 0.18);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: calc($unit / 2);
padding: $unit; padding: $unit;
min-width: 120px; min-width: 120px;
@ -49,7 +47,7 @@
overflow: hidden; overflow: hidden;
svg { svg {
fill: white; fill: var(--button-contained-bg);
filter: drop-shadow(0px 0px 1px rgb(0 0 0 / 0.18)); filter: drop-shadow(0px 0px 1px rgb(0 0 0 / 0.18));
} }
} }
@ -66,9 +64,9 @@
} }
.Label { .Label {
color: $grey-60; color: var(--text-tertiary);
font-size: $font-small; font-size: $font-small;
margin-bottom: calc($unit / 2); margin-bottom: $unit-half;
padding-left: calc($unit / 2); padding: $unit-half 0 $unit $unit-half;
} }
} }

View file

@ -1,29 +1,30 @@
.Item { .Item {
align-items: center; align-items: center;
border-radius: calc($unit / 2); border-radius: calc($unit / 2);
color: $grey-40; color: var(--text-secondary);
font-size: $font-regular; font-size: $font-regular;
line-height: 1.2; line-height: 1.2;
min-width: 100px; min-width: 100px;
position: relative; position: relative;
padding: $unit; padding: $unit;
padding-left: $unit * 3; padding-left: $unit * 3.5;
&:hover { &:hover {
background: $grey-90; background: var(--button-contained-bg-hover);
color: var(--text-primary);
cursor: pointer; cursor: pointer;
} }
&[data-state="checked"] { &[data-state='checked'] {
background: $grey-90; background: var(--button-contained-bg-hover);
svg { svg {
fill: $grey-50; fill: var(--text-secondary);
} }
} }
.Indicator { .Indicator {
$diameter: 18px; $diameter: 20px;
display: flex; display: flex;
align-items: center; align-items: center;

View file

@ -22,7 +22,8 @@ const SearchFilterCheckboxItem = (props: Props) => {
className="Item" className="Item"
checked={props.checked || false} checked={props.checked || false}
onCheckedChange={handleCheckedChange} onCheckedChange={handleCheckedChange}
onSelect={ (event) => event.preventDefault() }> onSelect={(event) => event.preventDefault()}
>
<DropdownMenu.ItemIndicator className="Indicator"> <DropdownMenu.ItemIndicator className="Indicator">
<CheckIcon /> <CheckIcon />
</DropdownMenu.ItemIndicator> </DropdownMenu.ItemIndicator>

View file

@ -20,6 +20,7 @@
} }
#Bar { #Bar {
align-items: center;
border-top-left-radius: $unit; border-top-left-radius: $unit;
border-top-right-radius: $unit; border-top-right-radius: $unit;
display: flex; display: flex;
@ -39,16 +40,16 @@
label { label {
width: 100%; width: 100%;
.Input { // .Input {
background: $grey-90; // background: $grey-90;
border: none; // border: none;
border-radius: calc($unit / 2); // border-radius: calc($unit / 2);
box-sizing: border-box; // box-sizing: border-box;
font-size: $font-regular; // font-size: $font-regular;
padding: $unit * 1.5; // padding: $unit * 1.5;
text-align: left; // text-align: left;
width: 100%; // width: 100%;
} // }
} }
} }
} }
@ -62,17 +63,17 @@
h5.total { h5.total {
font-size: $font-regular; font-size: $font-regular;
font-weight: $normal; font-weight: $normal;
color: $grey-40; color: var(--text-tertiary);
padding: calc($unit / 2) ($unit * 1.5); padding: $unit-half ($unit * 1.5);
} }
.footer { .footer {
align-items: center; align-items: center;
display: flex; display: flex;
color: $grey-60; color: var(--text-tertiary);
font-size: $font-regular; font-size: $font-regular;
font-weight: $normal; font-weight: $normal;
height: $unit * 10; height: $unit-10x;
justify-content: center; justify-content: center;
} }
@ -91,8 +92,8 @@
} }
.Search.Dialog #NoResults h2 { .Search.Dialog #NoResults h2 {
color: #ccc; color: var(--text-secondary);
font-size: $font-large; font-size: $font-large;
font-weight: 500; font-weight: 500;
margin-top: -32px; margin-top: $unit-4x * -1;
} }

View file

@ -1,35 +1,41 @@
import React, { useEffect, useState } from "react" import React, { useEffect, useState } from 'react'
import { getCookie, setCookie } from "cookies-next" import { getCookie, setCookie } from 'cookies-next'
import { useRouter } from "next/router" import { useRouter } from 'next/router'
import { useTranslation } from "react-i18next" import { useTranslation } from 'react-i18next'
import InfiniteScroll from "react-infinite-scroll-component" 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 Input from '~components/Input'
import WeaponSearchFilterBar from "~components/WeaponSearchFilterBar" import CharacterSearchFilterBar from '~components/CharacterSearchFilterBar'
import SummonSearchFilterBar from "~components/SummonSearchFilterBar" import WeaponSearchFilterBar from '~components/WeaponSearchFilterBar'
import JobSkillSearchFilterBar from "~components/JobSkillSearchFilterBar" import SummonSearchFilterBar from '~components/SummonSearchFilterBar'
import JobSkillSearchFilterBar from '~components/JobSkillSearchFilterBar'
import CharacterResult from "~components/CharacterResult" import CharacterResult from '~components/CharacterResult'
import WeaponResult from "~components/WeaponResult" import WeaponResult from '~components/WeaponResult'
import SummonResult from "~components/SummonResult" import SummonResult from '~components/SummonResult'
import JobSkillResult from "~components/JobSkillResult" import JobSkillResult from '~components/JobSkillResult'
import type { SearchableObject, SearchableObjectArray } from "~types" import type { SearchableObject, SearchableObjectArray } from '~types'
import "./index.scss" import './index.scss'
import CrossIcon from "~public/icons/Cross.svg" import CrossIcon from '~public/icons/Cross.svg'
import cloneDeep from "lodash.clonedeep" import cloneDeep from 'lodash.clonedeep'
interface Props { interface Props {
send: (object: SearchableObject, position: number) => any send: (object: SearchableObject, position: number) => any
placeholderText: string placeholderText: string
fromPosition: number fromPosition: number
job?: Job job?: Job
object: "weapons" | "characters" | "summons" | "job_skills" object: 'weapons' | 'characters' | 'summons' | 'job_skills'
children: React.ReactNode children: React.ReactNode
} }
@ -39,7 +45,7 @@ const SearchModal = (props: Props) => {
const locale = router.locale const locale = router.locale
// Set up translation // Set up translation
const { t } = useTranslation("common") const { t } = useTranslation('common')
let searchInput = React.createRef<HTMLInputElement>() let searchInput = React.createRef<HTMLInputElement>()
let scrollContainer = React.createRef<HTMLDivElement>() let scrollContainer = React.createRef<HTMLDivElement>()
@ -47,7 +53,7 @@ const SearchModal = (props: Props) => {
const [firstLoad, setFirstLoad] = useState(true) const [firstLoad, setFirstLoad] = useState(true)
const [filters, setFilters] = useState<{ [key: string]: any }>() const [filters, setFilters] = useState<{ [key: string]: any }>()
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const [query, setQuery] = useState("") const [query, setQuery] = useState('')
const [results, setResults] = useState<SearchableObjectArray>([]) const [results, setResults] = useState<SearchableObjectArray>([])
// Pagination states // Pagination states
@ -64,7 +70,7 @@ const SearchModal = (props: Props) => {
if (text.length) { if (text.length) {
setQuery(text) setQuery(text)
} else { } else {
setQuery("") setQuery('')
} }
} }
@ -113,7 +119,7 @@ const SearchModal = (props: Props) => {
: [] : []
let recents: SearchableObjectArray = [] let recents: SearchableObjectArray = []
if (props.object === "weapons") { if (props.object === 'weapons') {
recents = cloneDeep(cookieObj as Weapon[]) || [] recents = cloneDeep(cookieObj as Weapon[]) || []
if ( if (
!recents.find( !recents.find(
@ -123,7 +129,7 @@ const SearchModal = (props: Props) => {
) { ) {
recents.unshift(result as Weapon) recents.unshift(result as Weapon)
} }
} else if (props.object === "summons") { } else if (props.object === 'summons') {
recents = cloneDeep(cookieObj as Summon[]) || [] recents = cloneDeep(cookieObj as Summon[]) || []
if ( if (
!recents.find( !recents.find(
@ -136,7 +142,7 @@ const SearchModal = (props: Props) => {
} }
if (recents && recents.length > 5) recents.pop() if (recents && recents.length > 5) recents.pop()
setCookie(`recent_${props.object}`, recents, { path: "/" }) setCookie(`recent_${props.object}`, recents, { path: '/' })
sendData(result) sendData(result)
} }
@ -192,16 +198,16 @@ const SearchModal = (props: Props) => {
let jsx let jsx
switch (props.object) { switch (props.object) {
case "weapons": case 'weapons':
jsx = renderWeaponSearchResults() jsx = renderWeaponSearchResults()
break break
case "summons": case 'summons':
jsx = renderSummonSearchResults(results) jsx = renderSummonSearchResults(results)
break break
case "characters": case 'characters':
jsx = renderCharacterSearchResults(results) jsx = renderCharacterSearchResults(results)
break break
case "job_skills": case 'job_skills':
jsx = renderJobSkillSearchResults(results) jsx = renderJobSkillSearchResults(results)
break break
} }
@ -305,7 +311,7 @@ const SearchModal = (props: Props) => {
function openChange() { function openChange() {
if (open) { if (open) {
setQuery("") setQuery('')
setFirstLoad(true) setFirstLoad(true)
setResults([]) setResults([])
setRecordCount(0) setRecordCount(0)
@ -317,61 +323,54 @@ const SearchModal = (props: Props) => {
} }
return ( return (
<Dialog.Root open={open} onOpenChange={openChange}> <Dialog open={open} onOpenChange={openChange}>
<Dialog.Trigger asChild>{props.children}</Dialog.Trigger> <DialogTrigger asChild>{props.children}</DialogTrigger>
<Dialog.Portal> <DialogContent className="Search Dialog">
<Dialog.Content className="Search Dialog">
<div id="Header"> <div id="Header">
<div id="Bar"> <div id="Bar">
<label className="search_label" htmlFor="search_input"> <Input
<input
autoComplete="off" autoComplete="off"
type="text" className="Search"
name="query" name="query"
className="Input" placeholder={props.placeholderText}
id="search_input"
ref={searchInput} ref={searchInput}
value={query} value={query}
placeholder={props.placeholderText}
onChange={inputChanged} onChange={inputChanged}
/> />
</label> <DialogClose className="DialogClose" onClick={openChange}>
<Dialog.Close className="DialogClose" onClick={openChange}>
<CrossIcon /> <CrossIcon />
</Dialog.Close> </DialogClose>
</div> </div>
{props.object === "characters" ? ( {props.object === 'characters' ? (
<CharacterSearchFilterBar sendFilters={receiveFilters} /> <CharacterSearchFilterBar sendFilters={receiveFilters} />
) : ( ) : (
"" ''
)} )}
{props.object === "weapons" ? ( {props.object === 'weapons' ? (
<WeaponSearchFilterBar sendFilters={receiveFilters} /> <WeaponSearchFilterBar sendFilters={receiveFilters} />
) : ( ) : (
"" ''
)} )}
{props.object === "summons" ? ( {props.object === 'summons' ? (
<SummonSearchFilterBar sendFilters={receiveFilters} /> <SummonSearchFilterBar sendFilters={receiveFilters} />
) : ( ) : (
"" ''
)} )}
{props.object === "job_skills" ? ( {props.object === 'job_skills' ? (
<JobSkillSearchFilterBar sendFilters={receiveFilters} /> <JobSkillSearchFilterBar sendFilters={receiveFilters} />
) : ( ) : (
"" ''
)} )}
</div> </div>
<div id="Results" ref={scrollContainer}> <div id="Results" ref={scrollContainer}>
<h5 className="total"> <h5 className="total">
{t("search.result_count", { record_count: recordCount })} {t('search.result_count', { record_count: recordCount })}
</h5> </h5>
{open ? renderResults() : ""} {open ? renderResults() : ''}
</div> </div>
</Dialog.Content> </DialogContent>
<Dialog.Overlay className="Overlay" /> </Dialog>
</Dialog.Portal>
</Dialog.Root>
) )
} }

View file

@ -1,21 +1,21 @@
.Segment { .Segment {
color: $grey-50; color: $grey-55;
cursor: pointer; cursor: pointer;
font-size: 1.4rem; font-size: 1.4rem;
font-weight: $normal; font-weight: $normal;
min-width: 100px; min-width: 100px;
&:hover label { &:hover label {
background: $grey-90; background: var(--page-hover);
color: $grey-40; color: var(--text-primary);
} }
& input { & input {
display: none; display: none;
&:checked + label { &:checked + label {
background: $grey-90; background: var(--background);
color: $grey-00; color: var(--text-primary);
} }
} }

View file

@ -11,8 +11,6 @@ interface Props {
} }
const Segment: React.FC<Props> = (props: Props) => { const Segment: React.FC<Props> = (props: Props) => {
return ( return (
<div className="Segment"> <div className="Segment">
<input <input
@ -23,9 +21,7 @@ const Segment: React.FC<Props> = (props: Props) => {
checked={props.selected} checked={props.selected}
onChange={props.onClick} onChange={props.onClick}
/> />
<label htmlFor={props.name}> <label htmlFor={props.name}>{props.children}</label>
{props.children}
</label>
</div> </div>
) )
} }

View file

@ -4,7 +4,7 @@
} }
.SegmentedControl { .SegmentedControl {
background: white; background: var(--card-bg);
border-radius: $unit * 3; border-radius: $unit * 3;
display: inline-flex; display: inline-flex;
padding: 3px; padding: 3px;
@ -12,77 +12,76 @@
user-select: none; user-select: none;
overflow: hidden; overflow: hidden;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0); -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
z-index: 1;
&.fire { &.fire {
.Segment input:checked + label { .Segment input:checked + label {
background: $fire-bg-dark; background: $fire-bg-10;
color: $fire-text-dark; color: $fire-text-10;
} }
.Segment:hover label { .Segment:hover label {
background: $fire-bg-light; background: var(--fire-hover-bg);
color: $fire-text-light; color: var(--fire-hover-text);
} }
} }
&.water { &.water {
.Segment input:checked + label { .Segment input:checked + label {
background: $water-bg-dark; background: $water-bg-10;
color: $water-text-dark; color: $water-text-10;
} }
.Segment:hover label { .Segment:hover label {
background: $water-bg-light; background: var(--water-hover-bg);
color: $water-text-light; color: var(--water-hover-text);
} }
} }
&.earth { &.earth {
.Segment input:checked + label { .Segment input:checked + label {
background: $earth-bg-dark; background: $earth-bg-10;
color: $earth-text-dark; color: $earth-text-10;
} }
.Segment:hover label { .Segment:hover label {
background: $earth-bg-light; background: var(--earth-hover-bg);
color: $earth-text-light; color: var(--earth-hover-text);
} }
} }
&.wind { &.wind {
.Segment input:checked + label { .Segment input:checked + label {
background: $wind-bg-dark; background: $wind-bg-10;
color: $wind-text-dark; color: $wind-text-10;
} }
.Segment:hover label { .Segment:hover label {
background: $wind-bg-light; background: var(--wind-hover-bg);
color: $wind-text-light; color: var(--wind-hover-text);
} }
} }
&.light { &.light {
.Segment input:checked + label { .Segment input:checked + label {
background: $light-bg-dark; background: $light-bg-10;
color: $light-text-dark; color: $light-text-10;
} }
.Segment:hover label { .Segment:hover label {
background: $light-bg-light; background: var(--light-hover-bg);
color: $light-text-light; color: var(--light-hover-text);
} }
} }
&.dark { &.dark {
.Segment input:checked + label { .Segment input:checked + label {
background: $dark-bg-dark; background: $dark-bg-10;
color: $dark-text-dark; color: $dark-text-10;
} }
.Segment:hover label { .Segment:hover label {
background: $dark-bg-light; background: var(--dark-hover-bg);
color: $dark-text-light; color: var(--dark-hover-text);
} }
} }
} }

View file

@ -9,7 +9,7 @@ interface Props {
const SegmentedControl: React.FC<Props> = ({ elementClass, children }) => { const SegmentedControl: React.FC<Props> = ({ elementClass, children }) => {
return ( return (
<div className="SegmentedControlWrapper"> <div className="SegmentedControlWrapper">
<div className={`SegmentedControl ${(elementClass) ? elementClass : ''}`}> <div className={`SegmentedControl ${elementClass ? elementClass : ''}`}>
{children} {children}
</div> </div>
</div> </div>

View 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);
}
}

View 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

View 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;
}
}
}

View 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

View 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;
}
}

View 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

View file

@ -17,7 +17,7 @@
&:not(.btn-disabled) { &:not(.btn-disabled) {
background: $grey-90; background: $grey-90;
color: $grey-40; color: $grey-50;
&:hover { &:hover {
background: $grey-80; background: $grey-80;
@ -26,7 +26,7 @@
} }
.terms { .terms {
color: $grey-40; color: $grey-50;
font-size: $font-small; font-size: $font-small;
line-height: 1.2; line-height: 1.2;
margin-top: $unit; margin-top: $unit;

View file

@ -1,20 +1,20 @@
import React, { useEffect, useState } from "react" import React, { useEffect, useState } from 'react'
import Link from "next/link" import Link from 'next/link'
import { setCookie } from "cookies-next" import { setCookie } from 'cookies-next'
import { useRouter } from "next/router" import { useRouter } from 'next/router'
import { Trans, useTranslation } from "next-i18next" import { Trans, useTranslation } from 'next-i18next'
import { AxiosResponse } from "axios" 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 api from '~utils/api'
import { accountState } from "~utils/accountState" import { accountState } from '~utils/accountState'
import Button from "~components/Button" import Button from '~components/Button'
import Fieldset from "~components/Fieldset" import Fieldset from '~components/Input'
import CrossIcon from "~public/icons/Cross.svg" import CrossIcon from '~public/icons/Cross.svg'
import "./index.scss" import './index.scss'
interface Props {} interface Props {}
@ -31,15 +31,15 @@ const emailRegex =
const SignupModal = (props: Props) => { const SignupModal = (props: Props) => {
const router = useRouter() const router = useRouter()
const { t } = useTranslation("common") const { t } = useTranslation('common')
// Set up form states and error handling // Set up form states and error handling
const [formValid, setFormValid] = useState(false) const [formValid, setFormValid] = useState(false)
const [errors, setErrors] = useState<ErrorMap>({ const [errors, setErrors] = useState<ErrorMap>({
username: "", username: '',
email: "", email: '',
password: "", password: '',
passwordConfirmation: "", passwordConfirmation: '',
}) })
// States // States
@ -90,7 +90,7 @@ const SignupModal = (props: Props) => {
token: user.token, token: user.token,
} }
setCookie("account", cookieObj, { path: "/" }) setCookie('account', cookieObj, { path: '/' })
} }
function fetchUserInfo(id: string) { function fetchUserInfo(id: string) {
@ -108,7 +108,7 @@ const SignupModal = (props: Props) => {
} }
// TODO: Set language // TODO: Set language
setCookie("user", cookieObj, { path: "/" }) setCookie('user', cookieObj, { path: '/' })
accountState.account.user = { accountState.account.user = {
id: user.id, id: user.id,
@ -151,13 +151,13 @@ const SignupModal = (props: Props) => {
if (available) { if (available) {
// Continue checking for errors // Continue checking for errors
newErrors[fieldName] = "" newErrors[fieldName] = ''
setErrors(newErrors) setErrors(newErrors)
setFormValid(true) setFormValid(true)
validateName(fieldName, value) validateName(fieldName, value)
} else { } else {
newErrors[fieldName] = t("modals.signup.errors.field_in_use", { newErrors[fieldName] = t('modals.signup.errors.field_in_use', {
field: fieldName, field: fieldName,
}) })
setErrors(newErrors) setErrors(newErrors)
@ -169,19 +169,19 @@ const SignupModal = (props: Props) => {
let newErrors = { ...errors } let newErrors = { ...errors }
switch (fieldName) { switch (fieldName) {
case "username": case 'username':
if (value.length < 3) 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) else if (value.length > 20)
newErrors.username = t("modals.signup.errors.username_too_long") newErrors.username = t('modals.signup.errors.username_too_long')
else newErrors.username = "" else newErrors.username = ''
break break
case "email": case 'email':
newErrors.email = emailRegex.test(value) newErrors.email = emailRegex.test(value)
? "" ? ''
: t("modals.signup.errors.invalid_email") : t('modals.signup.errors.invalid_email')
break break
default: default:
@ -198,25 +198,25 @@ const SignupModal = (props: Props) => {
let newErrors = { ...errors } let newErrors = { ...errors }
switch (name) { switch (name) {
case "password": case 'password':
newErrors.password = passwordInput.current?.value.includes( newErrors.password = passwordInput.current?.value.includes(
usernameInput.current?.value! usernameInput.current?.value!
) )
? t("modals.signup.errors.password_contains_username") ? t('modals.signup.errors.password_contains_username')
: "" : ''
break break
case "password": case 'password':
newErrors.password = newErrors.password =
value.length < 8 ? t("modals.signup.errors.password_too_short") : "" value.length < 8 ? t('modals.signup.errors.password_too_short') : ''
break break
case "confirm_password": case 'confirm_password':
newErrors.passwordConfirmation = newErrors.passwordConfirmation =
passwordInput.current?.value === passwordInput.current?.value ===
passwordConfirmationInput.current?.value passwordConfirmationInput.current?.value
? "" ? ''
: t("modals.signup.errors.passwords_dont_match") : t('modals.signup.errors.passwords_dont_match')
break break
default: default:
@ -243,10 +243,10 @@ const SignupModal = (props: Props) => {
function openChange(open: boolean) { function openChange(open: boolean) {
setOpen(open) setOpen(open)
setErrors({ setErrors({
username: "", username: '',
email: "", email: '',
password: "", password: '',
passwordConfirmation: "", passwordConfirmation: '',
}) })
} }
@ -254,7 +254,7 @@ const SignupModal = (props: Props) => {
<Dialog.Root open={open} onOpenChange={openChange}> <Dialog.Root open={open} onOpenChange={openChange}>
<Dialog.Trigger asChild> <Dialog.Trigger asChild>
<li className="MenuItem"> <li className="MenuItem">
<span>{t("menu.signup")}</span> <span>{t('menu.signup')}</span>
</li> </li>
</Dialog.Trigger> </Dialog.Trigger>
<Dialog.Portal> <Dialog.Portal>
@ -264,7 +264,7 @@ const SignupModal = (props: Props) => {
> >
<div className="DialogHeader"> <div className="DialogHeader">
<Dialog.Title className="DialogTitle"> <Dialog.Title className="DialogTitle">
{t("modals.signup.title")} {t('modals.signup.title')}
</Dialog.Title> </Dialog.Title>
<Dialog.Close className="DialogClose" asChild> <Dialog.Close className="DialogClose" asChild>
<span> <span>
@ -276,7 +276,7 @@ const SignupModal = (props: Props) => {
<form className="form" onSubmit={register}> <form className="form" onSubmit={register}>
<Fieldset <Fieldset
fieldName="username" fieldName="username"
placeholder={t("modals.signup.placeholders.username")} placeholder={t('modals.signup.placeholders.username')}
onChange={handleNameChange} onChange={handleNameChange}
error={errors.username} error={errors.username}
ref={usernameInput} ref={usernameInput}
@ -284,7 +284,7 @@ const SignupModal = (props: Props) => {
<Fieldset <Fieldset
fieldName="email" fieldName="email"
placeholder={t("modals.signup.placeholders.email")} placeholder={t('modals.signup.placeholders.email')}
onChange={handleNameChange} onChange={handleNameChange}
error={errors.email} error={errors.email}
ref={emailInput} ref={emailInput}
@ -292,7 +292,7 @@ const SignupModal = (props: Props) => {
<Fieldset <Fieldset
fieldName="password" fieldName="password"
placeholder={t("modals.signup.placeholders.password")} placeholder={t('modals.signup.placeholders.password')}
onChange={handlePasswordChange} onChange={handlePasswordChange}
error={errors.password} error={errors.password}
ref={passwordInput} ref={passwordInput}
@ -300,13 +300,13 @@ const SignupModal = (props: Props) => {
<Fieldset <Fieldset
fieldName="confirm_password" fieldName="confirm_password"
placeholder={t("modals.signup.placeholders.password_confirm")} placeholder={t('modals.signup.placeholders.password_confirm')}
onChange={handlePasswordChange} onChange={handlePasswordChange}
error={errors.passwordConfirmation} error={errors.passwordConfirmation}
ref={passwordConfirmationInput} ref={passwordConfirmationInput}
/> />
<Button>{t("modals.signup.buttons.confirm")}</Button> <Button>{t('modals.signup.buttons.confirm')}</Button>
<Dialog.Description className="terms"> <Dialog.Description className="terms">
{/* <Trans i18nKey="modals.signup.agreement"> {/* <Trans i18nKey="modals.signup.agreement">

View file

@ -5,7 +5,7 @@
justify-content: center; justify-content: center;
& .Label { & .Label {
color: $grey-50; color: $grey-55;
font-size: $font-tiny; font-size: $font-tiny;
font-weight: $medium; font-weight: $medium;
margin-bottom: $unit; margin-bottom: $unit;

View file

@ -1,20 +1,20 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
import React, { useCallback, useEffect, useMemo, useState } from "react" import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { getCookie } from "cookies-next" import { getCookie } from 'cookies-next'
import { useSnapshot } from "valtio" import { useSnapshot } from 'valtio'
import { useTranslation } from "next-i18next" import { useTranslation } from 'next-i18next'
import { AxiosResponse } from "axios" import { AxiosResponse } from 'axios'
import debounce from "lodash.debounce" import debounce from 'lodash.debounce'
import SummonUnit from "~components/SummonUnit" import SummonUnit from '~components/SummonUnit'
import ExtraSummons from "~components/ExtraSummons" import ExtraSummons from '~components/ExtraSummons'
import api from "~utils/api" import api from '~utils/api'
import { appState } from "~utils/appState" import { appState } from '~utils/appState'
import type { SearchableObject } from "~types" import type { SearchableObject } from '~types'
import "./index.scss" import './index.scss'
// Props // Props
interface Props { interface Props {
@ -29,7 +29,7 @@ const SummonGrid = (props: Props) => {
const numSummons: number = 4 const numSummons: number = 4
// Cookies // Cookies
const cookie = getCookie("account") const cookie = getCookie('account')
const accountData: AccountCookie = cookie const accountData: AccountCookie = cookie
? JSON.parse(cookie as string) ? JSON.parse(cookie as string)
: null : null
@ -38,7 +38,7 @@ const SummonGrid = (props: Props) => {
: {} : {}
// Localization // Localization
const { t } = useTranslation("common") const { t } = useTranslation('common')
// Set up state for view management // Set up state for view management
const { party, grid } = useSnapshot(appState) const { party, grid } = useSnapshot(appState)
@ -141,7 +141,7 @@ const SummonGrid = (props: Props) => {
try { try {
if (uncapLevel != previousUncapValues[position]) 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) storeGridSummon(response.data.grid_summon)
}) })
} catch (error) { } catch (error) {
@ -217,7 +217,7 @@ const SummonGrid = (props: Props) => {
// Render: JSX components // Render: JSX components
const mainSummonElement = ( const mainSummonElement = (
<div className="LabeledUnit"> <div className="LabeledUnit">
<div className="Label">{t("summons.main")}</div> <div className="Label">{t('summons.main')}</div>
<SummonUnit <SummonUnit
gridSummon={grid.summons.mainSummon} gridSummon={grid.summons.mainSummon}
editable={party.editable} editable={party.editable}
@ -232,7 +232,7 @@ const SummonGrid = (props: Props) => {
const friendSummonElement = ( const friendSummonElement = (
<div className="LabeledUnit"> <div className="LabeledUnit">
<div className="Label">{t("summons.friend")}</div> <div className="Label">{t('summons.friend')}</div>
<SummonUnit <SummonUnit
gridSummon={grid.summons.friendSummon} gridSummon={grid.summons.friendSummon}
editable={party.editable} editable={party.editable}
@ -246,7 +246,7 @@ const SummonGrid = (props: Props) => {
) )
const summonGridElement = ( const summonGridElement = (
<div id="LabeledGrid"> <div id="LabeledGrid">
<div className="Label">{t("summons.summons")}</div> <div className="Label">{t('summons.summons')}</div>
<ul id="grid_summons"> <ul id="grid_summons">
{Array.from(Array(numSummons)).map((x, i) => { {Array.from(Array(numSummons)).map((x, i) => {
return ( return (

View file

@ -17,26 +17,39 @@ interface Props {
const SummonHovercard = (props: Props) => { const SummonHovercard = (props: Props) => {
const router = useRouter() const router = useRouter()
const { t } = useTranslation('common') const { t } = useTranslation('common')
const locale = (router.locale && ['en', 'ja'].includes(router.locale)) ? router.locale : 'en' 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 tintElement = Element[props.gridSummon.object.element]
const wikiUrl = `https://gbf.wiki/${props.gridSummon.object.name.en.replaceAll(' ', '_')}` const wikiUrl = `https://gbf.wiki/${props.gridSummon.object.name.en.replaceAll(
' ',
'_'
)}`
function summonImage() { function summonImage() {
let imgSrc = "" let imgSrc = ''
if (props.gridSummon) { if (props.gridSummon) {
const summon = props.gridSummon.object const summon = props.gridSummon.object
const upgradedSummons = [ const upgradedSummons = [
'2040094000', '2040100000', '2040080000', '2040098000', '2040094000',
'2040090000', '2040084000', '2040003000', '2040056000' '2040100000',
'2040080000',
'2040098000',
'2040090000',
'2040084000',
'2040003000',
'2040056000',
] ]
let suffix = '' let suffix = ''
if (upgradedSummons.indexOf(summon.granblue_id.toString()) != -1 && props.gridSummon.uncap_level == 5) if (
upgradedSummons.indexOf(summon.granblue_id.toString()) != -1 &&
props.gridSummon.uncap_level == 5
)
suffix = '_02' suffix = '_02'
// Generate the correct source for the summon // Generate the correct source for the summon
@ -48,18 +61,21 @@ const SummonHovercard = (props: Props) => {
return ( return (
<HoverCard.Root> <HoverCard.Root>
<HoverCard.Trigger> <HoverCard.Trigger>{props.children}</HoverCard.Trigger>
{ props.children }
</HoverCard.Trigger>
<HoverCard.Content className="Weapon Hovercard"> <HoverCard.Content className="Weapon Hovercard">
<div className="top"> <div className="top">
<div className="title"> <div className="title">
<h4>{ props.gridSummon.object.name[locale] }</h4> <h4>{props.gridSummon.object.name[locale]}</h4>
<img alt={props.gridSummon.object.name[locale]} src={summonImage()} /> <img
alt={props.gridSummon.object.name[locale]}
src={summonImage()}
/>
</div> </div>
<div className="subInfo"> <div className="subInfo">
<div className="icons"> <div className="icons">
<WeaponLabelIcon labelType={Element[props.gridSummon.object.element]}/> <WeaponLabelIcon
labelType={Element[props.gridSummon.object.element]}
/>
</div> </div>
<UncapIndicator <UncapIndicator
type="summon" type="summon"
@ -69,7 +85,9 @@ const SummonHovercard = (props: Props) => {
/> />
</div> </div>
</div> </div>
<a className={`Button ${tintElement}`} href={wikiUrl} target="_new">{t('buttons.wiki')}</a> <a className={`Button ${tintElement}`} href={wikiUrl} target="_new">
{t('buttons.wiki')}
</a>
<HoverCard.Arrow /> <HoverCard.Arrow />
</HoverCard.Content> </HoverCard.Content>
</HoverCard.Root> </HoverCard.Root>
@ -77,4 +95,3 @@ const SummonHovercard = (props: Props) => {
} }
export default SummonHovercard export default SummonHovercard

View file

@ -5,8 +5,12 @@
padding: $unit * 1.5; padding: $unit * 1.5;
&:hover { &:hover {
background: $grey-90; background: var(--button-contained-bg);
cursor: pointer; cursor: pointer;
.Info h5 {
color: var(--text-primary);
}
} }
img { img {
@ -21,10 +25,10 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex-grow: 1; flex-grow: 1;
gap: calc($unit / 2); gap: $unit-half;
h5 { h5 {
color: #555; color: var(--text-secondary);
display: inline-block; display: inline-block;
font-size: $font-medium; font-size: $font-medium;
font-weight: $medium; font-weight: $medium;
@ -37,11 +41,11 @@
.stars { .stars {
display: inline-block; display: inline-block;
color: #FFA15E; color: #ffa15e;
font-size: $font-xlarge; font-size: $font-xlarge;
& > span { & > span {
color: #65DAFF; color: #65daff;
} }
} }

View file

@ -15,13 +15,17 @@ const Element = ['null', 'wind', 'fire', 'water', 'earth', 'dark', 'light']
const SummonResult = (props: Props) => { const SummonResult = (props: Props) => {
const router = useRouter() const router = useRouter()
const locale = (router.locale && ['en', 'ja'].includes(router.locale)) ? router.locale : 'en' const locale =
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
const summon = props.data const summon = props.data
return ( return (
<li className="SummonResult" onClick={props.onClick}> <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`} /> <img
alt={summon.name[locale]}
src={`${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/summon-grid/${summon.granblue_id}.jpg`}
/>
<div className="Info"> <div className="Info">
<h5>{summon.name[locale]}</h5> <h5>{summon.name[locale]}</h5>
<UncapIndicator <UncapIndicator

View file

@ -23,7 +23,8 @@ const SummonSearchFilterBar = (props: Props) => {
const [elementMenu, setElementMenu] = useState(false) const [elementMenu, setElementMenu] = useState(false)
const [rarityState, setRarityState] = useState<RarityState>(emptyRarityState) const [rarityState, setRarityState] = useState<RarityState>(emptyRarityState)
const [elementState, setElementState] = useState<ElementState>(emptyElementState) const [elementState, setElementState] =
useState<ElementState>(emptyElementState)
function rarityMenuOpened(open: boolean) { function rarityMenuOpened(open: boolean) {
if (open) { if (open) {
@ -52,12 +53,16 @@ const SummonSearchFilterBar = (props: Props) => {
} }
function sendFilters() { function sendFilters() {
const checkedRarityFilters = Object.values(rarityState).filter(x => x.checked).map((x, i) => x.id) const checkedRarityFilters = Object.values(rarityState)
const checkedElementFilters = Object.values(elementState).filter(x => x.checked).map((x, i) => x.id) .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 = { const filters = {
rarity: checkedRarityFilters, rarity: checkedRarityFilters,
element: checkedElementFilters element: checkedElementFilters,
} }
props.sendFilters(filters) props.sendFilters(filters)
@ -69,34 +74,58 @@ const SummonSearchFilterBar = (props: Props) => {
return ( return (
<div className="SearchFilterBar"> <div className="SearchFilterBar">
<SearchFilter label={t('filters.labels.rarity')} numSelected={Object.values(rarityState).map(x => x.checked).filter(Boolean).length} open={rarityMenu} onOpenChange={rarityMenuOpened}> <SearchFilter
<DropdownMenu.Label className="Label">{t('filters.labels.rarity')}</DropdownMenu.Label> label={t('filters.labels.rarity')}
{ Array.from(Array(rarities.length)).map((x, i) => { 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 ( return (
<SearchFilterCheckboxItem <SearchFilterCheckboxItem
key={rarities[i]} key={rarities[i]}
onCheckedChange={handleRarityChange} onCheckedChange={handleRarityChange}
checked={rarityState[rarities[i]].checked} checked={rarityState[rarities[i]].checked}
valueKey={rarities[i]}> valueKey={rarities[i]}
>
{t(`rarities.${rarities[i]}`)} {t(`rarities.${rarities[i]}`)}
</SearchFilterCheckboxItem> </SearchFilterCheckboxItem>
)} )
) } })}
</SearchFilter> </SearchFilter>
<SearchFilter label={t('filters.labels.element')} numSelected={Object.values(elementState).map(x => x.checked).filter(Boolean).length} open={elementMenu} onOpenChange={elementMenuOpened}> <SearchFilter
<DropdownMenu.Label className="Label">{t('filters.labels.element')}</DropdownMenu.Label> label={t('filters.labels.element')}
{ Array.from(Array(elements.length)).map((x, i) => { 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 ( return (
<SearchFilterCheckboxItem <SearchFilterCheckboxItem
key={elements[i]} key={elements[i]}
onCheckedChange={handleElementChange} onCheckedChange={handleElementChange}
checked={elementState[elements[i]].checked} checked={elementState[elements[i]].checked}
valueKey={elements[i]}> valueKey={elements[i]}
>
{t(`elements.${elements[i]}`)} {t(`elements.${elements[i]}`)}
</SearchFilterCheckboxItem> </SearchFilterCheckboxItem>
)} )
) } })}
</SearchFilter> </SearchFilter>
</div> </div>
) )

View file

@ -52,7 +52,7 @@
} }
.SummonImage { .SummonImage {
background: white; background: var(--card-bg);
border: 1px solid rgba(0, 0, 0, 0); border: 1px solid rgba(0, 0, 0, 0);
border-radius: $unit; border-radius: $unit;
display: flex; display: flex;
@ -62,7 +62,7 @@
transition: all 0.18s ease-in-out; transition: all 0.18s ease-in-out;
&:hover .icon svg { &:hover .icon svg {
fill: $grey-40; fill: var(--icon-secondary-hover);
} }
.icon { .icon {
@ -72,7 +72,7 @@
z-index: 1; z-index: 1;
svg { svg {
fill: $grey-70; fill: var(--icon-secondary);
} }
} }
} }
@ -85,12 +85,13 @@
display: flex; display: flex;
} }
h3, ul { h3,
ul {
display: none; display: none;
} }
h3 { h3 {
color: #333; color: var(--text-primary);
font-size: $font-regular; font-size: $font-regular;
font-weight: $normal; font-weight: $normal;
line-height: 1.1; line-height: 1.1;

View file

@ -1,16 +1,16 @@
import React, { useEffect, useState } from "react" import React, { useEffect, useState } from 'react'
import { useRouter } from "next/router" import { useRouter } from 'next/router'
import { useTranslation } from "next-i18next" import { useTranslation } from 'next-i18next'
import classnames from "classnames" import classnames from 'classnames'
import SearchModal from "~components/SearchModal" import SearchModal from '~components/SearchModal'
import SummonHovercard from "~components/SummonHovercard" import SummonHovercard from '~components/SummonHovercard'
import UncapIndicator from "~components/UncapIndicator" import UncapIndicator from '~components/UncapIndicator'
import PlusIcon from "~public/icons/Add.svg" 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 { interface Props {
gridSummon: GridSummon | undefined gridSummon: GridSummon | undefined
@ -22,13 +22,13 @@ interface Props {
} }
const SummonUnit = (props: 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 router = useRouter()
const locale = 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({ const classes = classnames({
SummonUnit: true, SummonUnit: true,
@ -47,33 +47,33 @@ const SummonUnit = (props: Props) => {
}) })
function generateImageUrl() { function generateImageUrl() {
let imgSrc = "" let imgSrc = ''
if (props.gridSummon) { if (props.gridSummon) {
const summon = props.gridSummon.object! const summon = props.gridSummon.object!
const upgradedSummons = [ const upgradedSummons = [
"2040094000", '2040094000',
"2040100000", '2040100000',
"2040080000", '2040080000',
"2040098000", '2040098000',
"2040090000", '2040090000',
"2040084000", '2040084000',
"2040003000", '2040003000',
"2040056000", '2040056000',
"2040020000", '2040020000',
"2040034000", '2040034000',
"2040028000", '2040028000',
"2040027000", '2040027000',
"2040046000", '2040046000',
"2040047000", '2040047000',
] ]
let suffix = "" let suffix = ''
if ( if (
upgradedSummons.indexOf(summon.granblue_id.toString()) != -1 && upgradedSummons.indexOf(summon.granblue_id.toString()) != -1 &&
props.gridSummon.uncap_level == 5 props.gridSummon.uncap_level == 5
) )
suffix = "_02" suffix = '_02'
// Generate the correct source for the summon // Generate the correct source for the summon
if (props.unitType == 0 || props.unitType == 2) if (props.unitType == 0 || props.unitType == 2)
@ -98,14 +98,14 @@ const SummonUnit = (props: Props) => {
<PlusIcon /> <PlusIcon />
</span> </span>
) : ( ) : (
"" ''
)} )}
</div> </div>
) )
const editableImage = ( const editableImage = (
<SearchModal <SearchModal
placeholderText={t("search.placeholders.summon")} placeholderText={t('search.placeholders.summon')}
fromPosition={props.position} fromPosition={props.position}
object="summons" object="summons"
send={props.updateObject} send={props.updateObject}
@ -127,7 +127,7 @@ const SummonUnit = (props: Props) => {
special={false} special={false}
/> />
) : ( ) : (
"" ''
)} )}
<h3 className="SummonName">{summon?.name[locale]}</h3> <h3 className="SummonName">{summon?.name[locale]}</h3>
</div> </div>

View file

@ -1,5 +1,18 @@
.Fieldset textarea { .Fieldset textarea {
color: $grey-00; $offset: 2px;
font-family: system-ui, -apple-system, 'Helvetica Neue', Helvetica, Arial, sans-serif;
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; line-height: 21px;
-webkit-font-smoothing: antialiased;
padding: $unit-2x calc($unit-2x - $offset);
&:focus-within {
border: $offset solid $blue;
outline: none;
}
} }

View file

@ -10,7 +10,8 @@ interface Props {
onChange?: (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 ( return (
<fieldset className="Fieldset"> <fieldset className="Fieldset">
<textarea <textarea
@ -22,12 +23,10 @@ const TextFieldset = React.forwardRef<HTMLTextAreaElement, Props>(function field
onChange={props.onChange} onChange={props.onChange}
ref={ref} ref={ref}
/> />
{ {props.error.length > 0 && <p className="InputError">{props.error}</p>}
props.error.length > 0 &&
<p className='InputError'>{props.error}</p>
}
</fieldset> </fieldset>
) )
}) }
)
export default TextFieldset export default TextFieldset

View file

@ -1,5 +1,5 @@
.toggle-switch { .toggle-switch {
background: #fff; background: var(--card-bg);
border-radius: 18px; border-radius: 18px;
display: inline-block; display: inline-block;
position: relative; position: relative;
@ -30,7 +30,7 @@
} }
&-switch { &-switch {
background: #e4e4e4; background: var(--switch-nub); // #e4e4e4;
display: block; display: block;
width: 24px; width: 24px;
margin: 5px; margin: 5px;
@ -40,14 +40,18 @@
right: 24px; right: 24px;
border-radius: 17px; border-radius: 17px;
transition: all 0.18s ease-in 0s; transition: all 0.18s ease-in 0s;
&:hover {
background: var(--background);
}
} }
&-checkbox:checked + &-label { &-checkbox:checked + &-label {
background: #ECEBFF; background: var(--extra-purple-bg);
} }
&-checkbox:checked + &-label &-switch { &-checkbox:checked + &-label &-switch {
background: #8C86FF; background: var(--extra-purple-primary);
} }
&-checkbox:checked + &-label { &-checkbox:checked + &-label {

View file

@ -1,25 +1,30 @@
import React from "react" import React from 'react'
import { useSnapshot } from "valtio" import { useSnapshot } from 'valtio'
import { getCookie, deleteCookie } from "cookies-next" import { getCookie, deleteCookie } from 'cookies-next'
import { useRouter } from "next/router" import { useRouter } from 'next/router'
import { useTranslation } from "next-i18next" import { useTranslation } from 'next-i18next'
import clonedeep from "lodash.clonedeep" import clonedeep from 'lodash.clonedeep'
import api from "~utils/api" import api from '~utils/api'
import { accountState, initialAccountState } from "~utils/accountState" import { accountState, initialAccountState } from '~utils/accountState'
import { appState, initialAppState } from "~utils/appState" import { appState, initialAppState } from '~utils/appState'
import Header from "~components/Header" import Header from '~components/Header'
import Button from "~components/Button" import Button from '~components/Button'
import HeaderMenu from "~components/HeaderMenu" 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 TopHeader = () => {
const { t } = useTranslation("common") const { t } = useTranslation('common')
// Cookies // Cookies
const accountCookie = getCookie("account") const accountCookie = getCookie('account')
const userCookie = getCookie("user") const userCookie = getCookie('user')
const headers = {} const headers = {}
// accountCookies.account != null // accountCookies.account != null
@ -33,19 +38,19 @@ const TopHeader = () => {
const router = useRouter() const router = useRouter()
function copyToClipboard() { function copyToClipboard() {
const el = document.createElement("input") const el = document.createElement('input')
el.value = window.location.href el.value = window.location.href
el.id = "url-input" el.id = 'url-input'
document.body.appendChild(el) document.body.appendChild(el)
el.select() el.select()
document.execCommand("copy") document.execCommand('copy')
el.remove() el.remove()
} }
function newParty() { function newParty() {
// Push the root URL // Push the root URL
router.push("/") router.push('/')
// Clean state // Clean state
const resetState = clonedeep(initialAppState) const resetState = clonedeep(initialAppState)
@ -58,18 +63,18 @@ const TopHeader = () => {
} }
function logout() { function logout() {
deleteCookie("account") deleteCookie('account')
deleteCookie("user") deleteCookie('user')
// Clean state // Clean state
const resetState = clonedeep(initialAccountState) const resetState = clonedeep(initialAccountState)
Object.keys(resetState).forEach((key) => { 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 return false
} }
@ -83,7 +88,7 @@ const TopHeader = () => {
api.saveTeam({ id: party.id, params: headers }).then((response) => { api.saveTeam({ id: party.id, params: headers }).then((response) => {
if (response.status == 201) appState.party.favorited = true 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() { function unsaveFavorite() {
@ -91,13 +96,29 @@ const TopHeader = () => {
api.unsaveTeam({ id: party.id, params: headers }).then((response) => { api.unsaveTeam({ id: party.id, params: headers }).then((response) => {
if (response.status == 200) appState.party.favorited = false 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 = () => { const leftNav = () => {
return ( return (
<div className="dropdown"> <div className="dropdown">
<Button icon="menu">{t("buttons.menu")}</Button> <Button
accessoryIcon={<MenuIcon />}
blended={true}
text={t('buttons.menu')}
/>
{account.user ? ( {account.user ? (
<HeaderMenu <HeaderMenu
authenticated={account.authorized} authenticated={account.authorized}
@ -114,36 +135,41 @@ const TopHeader = () => {
const saveButton = () => { const saveButton = () => {
if (party.favorited) if (party.favorited)
return ( return (
<Button icon="save" active={true} onClick={toggleFavorite}> <Button
Saved accessoryIcon={<SaveIcon />}
</Button> blended={true}
text="Saved"
onClick={toggleFavorite}
/>
) )
else else
return ( return (
<Button icon="save" onClick={toggleFavorite}> <Button
Save accessoryIcon={<SaveIcon />}
</Button> blended={true}
text="Save"
onClick={toggleFavorite}
/>
) )
} }
const rightNav = () => { const rightNav = () => {
return ( return (
<div> <div>
{router.route === "/p/[party]" && {router.route === '/p/[party]' &&
account.user && account.user &&
(!party.user || party.user.id !== account.user.id) (!party.user || party.user.id !== account.user.id)
? saveButton() ? saveButton()
: ""} : ''}
{router.route === "/p/[party]" ? (
<Button icon="link" onClick={copyToClipboard}> {copyButton()}
{t("buttons.copy")}
</Button> <Button
) : ( accessoryIcon={<AddIcon className="Add" />}
"" blended={true}
)} text={t('buttons.new')}
<Button icon="new" onClick={newParty}> onClick={newParty}
{t("buttons.new")} />
</Button>
</div> </div>
) )
} }

View file

@ -1,10 +1,10 @@
import React, { useEffect, useRef, useState } from "react" import React, { useEffect, useRef, useState } from 'react'
import UncapStar from "~components/UncapStar" import UncapStar from '~components/UncapStar'
import "./index.scss" import './index.scss'
interface Props { interface Props {
type: "character" | "weapon" | "summon" type: 'character' | 'weapon' | 'summon'
rarity?: number rarity?: number
uncapLevel?: number uncapLevel?: number
flb: boolean flb: boolean
@ -20,7 +20,7 @@ const UncapIndicator = (props: Props) => {
function setNumStars() { function setNumStars() {
let numStars let numStars
if (props.type === "character") { if (props.type === 'character') {
if (props.special) { if (props.special) {
if (props.ulb) { if (props.ulb) {
numStars = 5 numStars = 5
@ -109,13 +109,13 @@ const UncapIndicator = (props: Props) => {
return ( return (
<ul className="UncapIndicator"> <ul className="UncapIndicator">
{Array.from(Array(numStars)).map((x, i) => { {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) if (props.special) return ulb(i)
else return transcendence(i) else return transcendence(i)
} else if ( } else if (
(props.special && props.type === "character" && i == 3) || (props.special && props.type === 'character' && i == 3) ||
(props.type === "character" && i == 4) || (props.type === 'character' && i == 4) ||
(props.type !== "character" && i > 2) (props.type !== 'character' && i > 2)
) { ) {
return flb(i) return flb(i)
} else { } else {

View file

@ -15,28 +15,25 @@ interface Props {
const UncapStar = (props: Props) => { const UncapStar = (props: Props) => {
const classes = classnames({ const classes = classnames({
UncapStar: true, UncapStar: true,
'empty': props.empty, empty: props.empty,
'special': props.special, special: props.special,
'mlb': !props.special, mlb: !props.special,
'flb': props.flb, flb: props.flb,
'ulb': props.ulb ulb: props.ulb,
}) })
function clicked() { function clicked() {
props.onClick(props.index, props.empty) props.onClick(props.index, props.empty)
} }
return ( return <li className={classes} onClick={clicked}></li>
<li className={classes} onClick={clicked}></li>
)
} }
UncapStar.defaultProps = { UncapStar.defaultProps = {
empty: false, empty: false,
special: false, special: false,
flb: false, flb: false,
ulb: false ulb: false,
} }
export default UncapStar export default UncapStar

View file

@ -12,7 +12,8 @@
} }
} }
#MainGrid, #ExtraGrid { #MainGrid,
#ExtraGrid {
.grid_weapons > * { .grid_weapons > * {
margin-bottom: $unit * 3; margin-bottom: $unit * 3;
margin-right: $unit * 3; margin-right: $unit * 3;
@ -22,12 +23,12 @@
margin-right: $unit * 2; margin-right: $unit * 2;
} }
&:nth-last-child(-n+3) { &:nth-last-child(-n + 3) {
margin-bottom: 0; margin-bottom: 0;
} }
} }
.grid_weapons > *:nth-child(3n+3) { .grid_weapons > *:nth-child(3n + 3) {
margin-right: 0; margin-right: 0;
} }
@ -39,4 +40,3 @@
#ExtraWeapons #grid_weapons > * { #ExtraWeapons #grid_weapons > * {
margin-bottom: 0; margin-bottom: 0;
} }

View file

@ -1,20 +1,20 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
import React, { useCallback, useEffect, useMemo, useState } from "react" import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { getCookie } from "cookies-next" import { getCookie } from 'cookies-next'
import { useSnapshot } from "valtio" import { useSnapshot } from 'valtio'
import { AxiosResponse } from "axios" import { AxiosResponse } from 'axios'
import debounce from "lodash.debounce" import debounce from 'lodash.debounce'
import WeaponUnit from "~components/WeaponUnit" import WeaponUnit from '~components/WeaponUnit'
import ExtraWeapons from "~components/ExtraWeapons" import ExtraWeapons from '~components/ExtraWeapons'
import api from "~utils/api" import api from '~utils/api'
import { appState } from "~utils/appState" import { appState } from '~utils/appState'
import type { SearchableObject } from "~types" import type { SearchableObject } from '~types'
import "./index.scss" import './index.scss'
// Props // Props
interface Props { interface Props {
@ -29,7 +29,7 @@ const WeaponGrid = (props: Props) => {
const numWeapons: number = 9 const numWeapons: number = 9
// Cookies // Cookies
const cookie = getCookie("account") const cookie = getCookie('account')
const accountData: AccountCookie = cookie const accountData: AccountCookie = cookie
? JSON.parse(cookie as string) ? JSON.parse(cookie as string)
: null : null
@ -132,7 +132,7 @@ const WeaponGrid = (props: Props) => {
try { try {
if (uncapLevel != previousUncapValues[position]) if (uncapLevel != previousUncapValues[position])
await api.updateUncap("weapon", id, uncapLevel).then((response) => { await api.updateUncap('weapon', id, uncapLevel).then((response) => {
storeGridWeapon(response.data.grid_weapon) storeGridWeapon(response.data.grid_weapon)
}) })
} catch (error) { } catch (error) {
@ -250,7 +250,7 @@ const WeaponGrid = (props: Props) => {
</div> </div>
{(() => { {(() => {
return party.extra ? extraGridElement : "" return party.extra ? extraGridElement : ''
})()} })()}
</div> </div>
) )

View file

@ -19,7 +19,7 @@ interface Props {
interface KeyNames { interface KeyNames {
[key: string]: { [key: string]: {
[key: string]: string [key: string]: string
en: string, en: string
ja: string ja: string
} }
} }
@ -27,39 +27,56 @@ interface KeyNames {
const WeaponHovercard = (props: Props) => { const WeaponHovercard = (props: Props) => {
const router = useRouter() const router = useRouter()
const { t } = useTranslation('common') const { t } = useTranslation('common')
const locale = (router.locale && ['en', 'ja'].includes(router.locale)) ? router.locale : 'en' 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 Proficiency = ['none', 'sword', 'dagger', 'axe', 'spear', 'bow', 'staff', 'fist', 'harp', 'gun', 'katana'] const Proficiency = [
'none',
'sword',
'dagger',
'axe',
'spear',
'bow',
'staff',
'fist',
'harp',
'gun',
'katana',
]
const WeaponKeyNames: KeyNames = { const WeaponKeyNames: KeyNames = {
'2': { '2': {
en: 'Pendulum', en: 'Pendulum',
ja: 'ペンデュラム' ja: 'ペンデュラム',
}, },
'3': { '3': {
en: 'Teluma', en: 'Teluma',
ja: 'テルマ' ja: 'テルマ',
}, },
'17': { '17': {
en: 'Gauph Key', en: 'Gauph Key',
ja: 'ガフスキー' ja: 'ガフスキー',
}, },
'22': { '22': {
en: 'Emblem', en: 'Emblem',
ja: 'エンブレム' ja: 'エンブレム',
} },
} }
const tintElement = (props.gridWeapon.object.element == 0 && props.gridWeapon.element) ? Element[props.gridWeapon.element] : Element[props.gridWeapon.object.element] const tintElement =
const wikiUrl = `https://gbf.wiki/${props.gridWeapon.object.name.en.replaceAll(' ', '_')}` props.gridWeapon.object.element == 0 && props.gridWeapon.element
? Element[props.gridWeapon.element]
: Element[props.gridWeapon.object.element]
const wikiUrl = `https://gbf.wiki/${props.gridWeapon.object.name.en.replaceAll(
' ',
'_'
)}`
const hovercardSide = () => { const hovercardSide = () => {
if (props.gridWeapon.position == -1) if (props.gridWeapon.position == -1) return 'right'
return "right"
else if ([6, 7, 8, 9, 10, 11].includes(props.gridWeapon.position)) else if ([6, 7, 8, 9, 10, 11].includes(props.gridWeapon.position))
return "top" return 'top'
else else return 'bottom'
return "bottom"
} }
const createPrimaryAxSkillString = () => { const createPrimaryAxSkillString = () => {
@ -67,9 +84,13 @@ const WeaponHovercard = (props: Props) => {
if (props.gridWeapon.ax) { if (props.gridWeapon.ax) {
const simpleAxSkill = props.gridWeapon.ax[0] const simpleAxSkill = props.gridWeapon.ax[0]
const axSkill = primaryAxSkills.find(skill => skill.id == simpleAxSkill.modifier) const axSkill = primaryAxSkills.find(
(skill) => skill.id == simpleAxSkill.modifier
)
return `${axSkill?.name[locale]} +${simpleAxSkill.strength}${ (axSkill?.suffix) ? axSkill.suffix : '' }` return `${axSkill?.name[locale]} +${simpleAxSkill.strength}${
axSkill?.suffix ? axSkill.suffix : ''
}`
} }
return '' return ''
@ -82,11 +103,17 @@ const WeaponHovercard = (props: Props) => {
const primarySimpleAxSkill = props.gridWeapon.ax[0] const primarySimpleAxSkill = props.gridWeapon.ax[0]
const secondarySimpleAxSkill = props.gridWeapon.ax[1] const secondarySimpleAxSkill = props.gridWeapon.ax[1]
const primaryAxSkill = primaryAxSkills.find(skill => skill.id == primarySimpleAxSkill.modifier) const primaryAxSkill = primaryAxSkills.find(
(skill) => skill.id == primarySimpleAxSkill.modifier
)
if (primaryAxSkill && primaryAxSkill.secondary) { if (primaryAxSkill && primaryAxSkill.secondary) {
const secondaryAxSkill = primaryAxSkill.secondary.find(skill => skill.id == secondarySimpleAxSkill.modifier) const secondaryAxSkill = primaryAxSkill.secondary.find(
return `${secondaryAxSkill?.name[locale]} +${secondarySimpleAxSkill.strength}${ (secondaryAxSkill?.suffix) ? secondaryAxSkill.suffix : '' }` (skill) => skill.id == secondarySimpleAxSkill.modifier
)
return `${secondaryAxSkill?.name[locale]} +${
secondarySimpleAxSkill.strength
}${secondaryAxSkill?.suffix ? secondaryAxSkill.suffix : ''}`
} }
} }
@ -104,18 +131,27 @@ const WeaponHovercard = (props: Props) => {
const keysSection = ( const keysSection = (
<section className="weaponKeys"> <section className="weaponKeys">
{ (WeaponKeyNames[props.gridWeapon.object.series]) ? {WeaponKeyNames[props.gridWeapon.object.series] ? (
<h5 className={tintElement}>{ WeaponKeyNames[props.gridWeapon.object.series][locale] }{ (locale === 'en') ? 's' : '' }</h5> : '' <h5 className={tintElement}>
} {WeaponKeyNames[props.gridWeapon.object.series][locale]}
{locale === 'en' ? 's' : ''}
</h5>
) : (
''
)}
{ (props.gridWeapon.weapon_keys) ? {props.gridWeapon.weapon_keys
Array.from(Array(props.gridWeapon.weapon_keys.length)).map((x, i) => { ? Array.from(Array(props.gridWeapon.weapon_keys.length)).map((x, i) => {
return ( return (
<div className="weaponKey" key={props.gridWeapon.weapon_keys![i].id}> <div
className="weaponKey"
key={props.gridWeapon.weapon_keys![i].id}
>
<span>{props.gridWeapon.weapon_keys![i].name[locale]}</span> <span>{props.gridWeapon.weapon_keys![i].name[locale]}</span>
</div> </div>
) )
}) : '' } })
: ''}
</section> </section>
) )
@ -124,36 +160,65 @@ const WeaponHovercard = (props: Props) => {
<h5 className={tintElement}>{t('modals.weapon.subtitles.ax_skills')}</h5> <h5 className={tintElement}>{t('modals.weapon.subtitles.ax_skills')}</h5>
<div className="skills"> <div className="skills">
<div className="primary axSkill"> <div className="primary axSkill">
<img alt="AX1" src={`/icons/ax/primary_${ (props.gridWeapon.ax) ? props.gridWeapon.ax[0].modifier : '' }.png`} /> <img
alt="AX1"
src={`/icons/ax/primary_${
props.gridWeapon.ax ? props.gridWeapon.ax[0].modifier : ''
}.png`}
/>
<span>{createPrimaryAxSkillString()}</span> <span>{createPrimaryAxSkillString()}</span>
</div> </div>
{ (props.gridWeapon.ax && props.gridWeapon.ax[1].modifier && props.gridWeapon.ax[1].strength) ? {props.gridWeapon.ax &&
props.gridWeapon.ax[1].modifier &&
props.gridWeapon.ax[1].strength ? (
<div className="secondary axSkill"> <div className="secondary axSkill">
<img alt="AX2" src={`/icons/ax/secondary_${ (props.gridWeapon.ax) ? props.gridWeapon.ax[1].modifier : '' }.png`} /> <img
alt="AX2"
src={`/icons/ax/secondary_${
props.gridWeapon.ax ? props.gridWeapon.ax[1].modifier : ''
}.png`}
/>
<span>{createSecondaryAxSkillString()}</span> <span>{createSecondaryAxSkillString()}</span>
</div> : ''} </div>
) : (
''
)}
</div> </div>
</section> </section>
) )
return ( return (
<HoverCard.Root> <HoverCard.Root>
<HoverCard.Trigger> <HoverCard.Trigger>{props.children}</HoverCard.Trigger>
{ props.children }
</HoverCard.Trigger>
<HoverCard.Content className="Weapon Hovercard" side={hovercardSide()}> <HoverCard.Content className="Weapon Hovercard" side={hovercardSide()}>
<div className="top"> <div className="top">
<div className="title"> <div className="title">
<h4>{ props.gridWeapon.object.name[locale] }</h4> <h4>{props.gridWeapon.object.name[locale]}</h4>
<img alt={props.gridWeapon.object.name[locale]} src={weaponImage()} /> <img
alt={props.gridWeapon.object.name[locale]}
src={weaponImage()}
/>
</div> </div>
<div className="subInfo"> <div className="subInfo">
<div className="icons"> <div className="icons">
{ (props.gridWeapon.object.element !== 0 || (props.gridWeapon.object.element === 0 && props.gridWeapon.element != null)) ? {props.gridWeapon.object.element !== 0 ||
<WeaponLabelIcon labelType={ (props.gridWeapon.object.element === 0 && props.gridWeapon.element !== 0) ? Element[props.gridWeapon.element] : Element[props.gridWeapon.object.element] } /> (props.gridWeapon.object.element === 0 &&
: '' } props.gridWeapon.element != null) ? (
<WeaponLabelIcon labelType={ Proficiency[props.gridWeapon.object.proficiency] } /> <WeaponLabelIcon
labelType={
props.gridWeapon.object.element === 0 &&
props.gridWeapon.element !== 0
? Element[props.gridWeapon.element]
: Element[props.gridWeapon.object.element]
}
/>
) : (
''
)}
<WeaponLabelIcon
labelType={Proficiency[props.gridWeapon.object.proficiency]}
/>
</div> </div>
<UncapIndicator <UncapIndicator
type="weapon" type="weapon"
@ -164,9 +229,18 @@ const WeaponHovercard = (props: Props) => {
</div> </div>
</div> </div>
{ (props.gridWeapon.object.ax > 0 && props.gridWeapon.ax && props.gridWeapon.ax[0].modifier && props.gridWeapon.ax[0].strength ) ? axSection : '' } {props.gridWeapon.object.ax > 0 &&
{ (props.gridWeapon.weapon_keys && props.gridWeapon.weapon_keys.length > 0) ? keysSection : '' } props.gridWeapon.ax &&
<a className={`Button ${tintElement}`} href={wikiUrl} target="_new">{t('buttons.wiki')}</a> props.gridWeapon.ax[0].modifier &&
props.gridWeapon.ax[0].strength
? axSection
: ''}
{props.gridWeapon.weapon_keys && props.gridWeapon.weapon_keys.length > 0
? keysSection
: ''}
<a className={`Button ${tintElement}`} href={wikiUrl} target="_new">
{t('buttons.wiki')}
</a>
<HoverCard.Arrow /> <HoverCard.Arrow />
</HoverCard.Content> </HoverCard.Content>
</HoverCard.Root> </HoverCard.Root>
@ -174,4 +248,3 @@ const WeaponHovercard = (props: Props) => {
} }
export default WeaponHovercard export default WeaponHovercard

View file

@ -13,49 +13,51 @@ interface Props {
onBlur?: (event: React.ChangeEvent<HTMLSelectElement>) => void onBlur?: (event: React.ChangeEvent<HTMLSelectElement>) => void
} }
const WeaponKeyDropdown = React.forwardRef<HTMLSelectElement, Props>(function useFieldSet(props, ref) { const WeaponKeyDropdown = React.forwardRef<HTMLSelectElement, Props>(
function useFieldSet(props, ref) {
const [keys, setKeys] = useState<WeaponKey[][]>([]) const [keys, setKeys] = useState<WeaponKey[][]>([])
const [currentKey, setCurrentKey] = useState('') const [currentKey, setCurrentKey] = useState('')
const pendulumNames = [ const pendulumNames = [
{ en: 'Pendulum', jp: '' }, { en: 'Pendulum', jp: '' },
{ en: 'Chain', jp: '' } { en: 'Chain', jp: '' },
] ]
const telumaNames = [ { en: 'Teluma', jp: '' } ] const telumaNames = [{ en: 'Teluma', jp: '' }]
const emblemNames = [ { en: 'Emblem', jp: '' } ] const emblemNames = [{ en: 'Emblem', jp: '' }]
const gauphNames = [ const gauphNames = [
{ en: 'Gauph Key', jp: '' }, { en: 'Gauph Key', jp: '' },
{ en: 'Ultima Key', jp: '' }, { en: 'Ultima Key', jp: '' },
{ en: 'Gate of Omnipotence', jp: '' } { en: 'Gate of Omnipotence', jp: '' },
] ]
useEffect(() => { useEffect(() => {
if (props.currentValue) if (props.currentValue) setCurrentKey(props.currentValue.id)
setCurrentKey(props.currentValue.id)
}, [props.currentValue]) }, [props.currentValue])
useEffect(() => { useEffect(() => {
const filterParams = { const filterParams = {
params: { params: {
series: props.series, series: props.series,
slot: props.slot slot: props.slot,
} },
} }
function organizeWeaponKeys(weaponKeys: WeaponKey[]) { function organizeWeaponKeys(weaponKeys: WeaponKey[]) {
const numGroups = Math.max.apply(Math, weaponKeys.map(key => key.group)) const numGroups = Math.max.apply(
Math,
weaponKeys.map((key) => key.group)
)
let groupedKeys = [] let groupedKeys = []
for (let i = 0; i <= numGroups; i++) { for (let i = 0; i <= numGroups; i++) {
groupedKeys[i] = weaponKeys.filter(key => key.group == i) groupedKeys[i] = weaponKeys.filter((key) => key.group == i)
} }
setKeys(groupedKeys) setKeys(groupedKeys)
} }
function fetchWeaponKeys() { function fetchWeaponKeys() {
api.endpoints.weapon_keys.getAll(filterParams) api.endpoints.weapon_keys.getAll(filterParams).then((response) => {
.then((response) => {
const keys = response.data.map((k: any) => k.weapon_key) const keys = response.data.map((k: any) => k.weapon_key)
organizeWeaponKeys(keys) organizeWeaponKeys(keys)
}) })
@ -65,53 +67,55 @@ const WeaponKeyDropdown = React.forwardRef<HTMLSelectElement, Props>(function us
}, [props.series, props.slot]) }, [props.series, props.slot])
function weaponKeyGroup(index: number) { function weaponKeyGroup(index: number) {
['α','β','γ','Δ'].sort((a, b) => a.localeCompare(b, 'el')) ;['α', 'β', 'γ', 'Δ'].sort((a, b) => a.localeCompare(b, 'el'))
const sortByOrder = (a: WeaponKey, b: WeaponKey) => a.order > b.order ? 1 : -1 const sortByOrder = (a: WeaponKey, b: WeaponKey) =>
a.order > b.order ? 1 : -1
const options = keys && keys.length > 0 && keys[index].length > 0 && const options =
keys &&
keys.length > 0 &&
keys[index].length > 0 &&
keys[index].sort(sortByOrder).map((item, i) => { keys[index].sort(sortByOrder).map((item, i) => {
return ( return (
<option key={i} value={item.id}>{item.name.en}</option> <option key={i} value={item.id}>
{item.name.en}
</option>
) )
}) })
let name: { [key: string]: string } = {} let name: { [key: string]: string } = {}
if (props.series == 2 && index == 0) if (props.series == 2 && index == 0) name = pendulumNames[0]
name = pendulumNames[0]
else if (props.series == 2 && props.slot == 1 && index == 1) else if (props.series == 2 && props.slot == 1 && index == 1)
name = pendulumNames[1] name = pendulumNames[1]
else if (props.series == 3) else if (props.series == 3) name = telumaNames[index]
name = telumaNames[index] else if (props.series == 17) name = gauphNames[props.slot]
else if (props.series == 17) else if (props.series == 22) name = emblemNames[index]
name = gauphNames[props.slot]
else if (props.series == 22)
name = emblemNames[index]
return ( return (
<optgroup key={index} label={ (props.series == 17 && props.slot == 2) ? name.en : `${name.en}s`}> <optgroup
key={index}
label={
props.series == 17 && props.slot == 2 ? name.en : `${name.en}s`
}
>
{options} {options}
</optgroup> </optgroup>
) )
} }
function handleChange(event: React.ChangeEvent<HTMLSelectElement>) { function handleChange(event: React.ChangeEvent<HTMLSelectElement>) {
if (props.onChange) if (props.onChange) props.onChange(event)
props.onChange(event)
setCurrentKey(event.currentTarget.value) setCurrentKey(event.currentTarget.value)
} }
const emptyOption = () => { const emptyOption = () => {
let name = '' let name = ''
if (props.series == 2) if (props.series == 2) name = pendulumNames[0].en
name = pendulumNames[0].en else if (props.series == 3) name = telumaNames[0].en
else if (props.series == 3) else if (props.series == 17) name = gauphNames[props.slot].en
name = telumaNames[0].en else if (props.series == 22) name = emblemNames[0].en
else if (props.series == 17)
name = gauphNames[props.slot].en
else if (props.series == 22)
name = emblemNames[0].en
return `No ${name}` return `No ${name}`
} }
@ -122,13 +126,17 @@ const WeaponKeyDropdown = React.forwardRef<HTMLSelectElement, Props>(function us
value={currentKey} value={currentKey}
onBlur={props.onBlur} onBlur={props.onBlur}
onChange={handleChange} onChange={handleChange}
ref={ref}> ref={ref}
<option key="-1" value="-1">{ emptyOption() }</option> >
{ Array.from(Array(keys?.length)).map((x, i) => { <option key="-1" value="-1">
{emptyOption()}
</option>
{Array.from(Array(keys?.length)).map((x, i) => {
return weaponKeyGroup(i) return weaponKeyGroup(i)
})} })}
</select> </select>
) )
}) }
)
export default WeaponKeyDropdown export default WeaponKeyDropdown

View file

@ -7,141 +7,140 @@
/* Elements */ /* Elements */
&.fire.en { &.fire.en {
background-image: url('/labels/element/fire_en.png') background-image: url('/labels/element/fire_en.png');
} }
&.fire.ja { &.fire.ja {
background-image: url('/labels/element/fire_ja.png') background-image: url('/labels/element/fire_ja.png');
} }
&.water.en { &.water.en {
background-image: url('/labels/element/water_en.png') background-image: url('/labels/element/water_en.png');
} }
&.water.ja { &.water.ja {
background-image: url('/labels/element/water_ja.png') background-image: url('/labels/element/water_ja.png');
} }
&.earth.en { &.earth.en {
background-image: url('/labels/element/earth_en.png') background-image: url('/labels/element/earth_en.png');
} }
&.earth.ja { &.earth.ja {
background-image: url('/labels/element/earth_ja.png') background-image: url('/labels/element/earth_ja.png');
} }
&.wind.en { &.wind.en {
background-image: url('/labels/element/wind_en.png') background-image: url('/labels/element/wind_en.png');
} }
&.wind.ja { &.wind.ja {
background-image: url('/labels/element/wind_ja.png') background-image: url('/labels/element/wind_ja.png');
} }
&.dark.en { &.dark.en {
background-image: url('/labels/element/dark_en.png') background-image: url('/labels/element/dark_en.png');
} }
&.dark.ja { &.dark.ja {
background-image: url('/labels/element/dark_ja.png') background-image: url('/labels/element/dark_ja.png');
} }
&.light.en { &.light.en {
background-image: url('/labels/element/light_en.png') background-image: url('/labels/element/light_en.png');
} }
&.light.ja { &.light.ja {
background-image: url('/labels/element/light_ja.png') background-image: url('/labels/element/light_ja.png');
} }
&.null.en { &.null.en {
background-image: url('/labels/element/any_en.png') background-image: url('/labels/element/any_en.png');
} }
&.null.ja { &.null.ja {
background-image: url('/labels/element/any_ja.png') background-image: url('/labels/element/any_ja.png');
} }
/* Proficiencies */ /* Proficiencies */
&.sword.en { &.sword.en {
background-image: url('/labels/proficiency/sabre_en.png') background-image: url('/labels/proficiency/sabre_en.png');
} }
&.sword.ja { &.sword.ja {
background-image: url('/labels/proficiency/sabre_ja.png') background-image: url('/labels/proficiency/sabre_ja.png');
} }
&.dagger.en { &.dagger.en {
background-image: url('/labels/proficiency/dagger_en.png') background-image: url('/labels/proficiency/dagger_en.png');
} }
&.dagger.ja { &.dagger.ja {
background-image: url('/labels/proficiency/dagger_ja.png') background-image: url('/labels/proficiency/dagger_ja.png');
} }
&.axe.en { &.axe.en {
background-image: url('/labels/proficiency/axe_en.png') background-image: url('/labels/proficiency/axe_en.png');
} }
&.axe.ja { &.axe.ja {
background-image: url('/labels/proficiency/axe_ja.png') background-image: url('/labels/proficiency/axe_ja.png');
} }
&.spear.en { &.spear.en {
background-image: url('/labels/proficiency/spear_en.png') background-image: url('/labels/proficiency/spear_en.png');
} }
&.spear.ja { &.spear.ja {
background-image: url('/labels/proficiency/spear_ja.png') background-image: url('/labels/proficiency/spear_ja.png');
} }
&.staff.en { &.staff.en {
background-image: url('/labels/proficiency/staff_en.png') background-image: url('/labels/proficiency/staff_en.png');
} }
&.staff.ja { &.staff.ja {
background-image: url('/labels/proficiency/staff_ja.png') background-image: url('/labels/proficiency/staff_ja.png');
} }
&.fist.en { &.fist.en {
background-image: url('/labels/proficiency/melee_en.png') background-image: url('/labels/proficiency/melee_en.png');
} }
&.fist.ja { &.fist.ja {
background-image: url('/labels/proficiency/melee_ja.png') background-image: url('/labels/proficiency/melee_ja.png');
} }
&.harp.en { &.harp.en {
background-image: url('/labels/proficiency/harp_en.png') background-image: url('/labels/proficiency/harp_en.png');
} }
&.harp.ja { &.harp.ja {
background-image: url('/labels/proficiency/harp_ja.png') background-image: url('/labels/proficiency/harp_ja.png');
} }
&.gun.en { &.gun.en {
background-image: url('/labels/proficiency/gun_en.png') background-image: url('/labels/proficiency/gun_en.png');
} }
&.gun.ja { &.gun.ja {
background-image: url('/labels/proficiency/gun_ja.png') background-image: url('/labels/proficiency/gun_ja.png');
} }
&.bow.en { &.bow.en {
background-image: url('/labels/proficiency/bow_en.png') background-image: url('/labels/proficiency/bow_en.png');
} }
&.bow.ja { &.bow.ja {
background-image: url('/labels/proficiency/bow_ja.png') background-image: url('/labels/proficiency/bow_ja.png');
} }
&.katana.en { &.katana.en {
background-image: url('/labels/proficiency/katana_en.png') background-image: url('/labels/proficiency/katana_en.png');
} }
&.katana.ja { &.katana.ja {
background-image: url('/labels/proficiency/katana_ja.png') background-image: url('/labels/proficiency/katana_ja.png');
} }
} }

View file

@ -10,9 +10,7 @@ interface Props {
const WeaponLabelIcon = (props: Props) => { const WeaponLabelIcon = (props: Props) => {
const router = useRouter() const router = useRouter()
return ( return <i className={`WeaponLabelIcon ${props.labelType} ${router.locale}`} />
<i className={`WeaponLabelIcon ${props.labelType} ${router.locale}`} />
)
} }
export default WeaponLabelIcon export default WeaponLabelIcon

View file

@ -10,7 +10,7 @@
gap: calc($unit / 2); gap: calc($unit / 2);
h3 { h3 {
color: $grey-50; color: $grey-55;
font-size: $font-small; font-size: $font-small;
margin-bottom: $unit; margin-bottom: $unit;
} }
@ -33,7 +33,7 @@
&:not(.btn-disabled) { &:not(.btn-disabled) {
background: $grey-90; background: $grey-90;
color: $grey-40; color: $grey-50;
&:hover { &:hover {
background: $grey-80; background: $grey-80;

View file

@ -1,21 +1,21 @@
import React, { useState } from "react" import React, { useState } from 'react'
import { getCookie } from "cookies-next" import { getCookie } from 'cookies-next'
import { useRouter } from "next/router" import { useRouter } from 'next/router'
import { useTranslation } from "next-i18next" import { useTranslation } from 'next-i18next'
import { AxiosResponse } from "axios" import { AxiosResponse } from 'axios'
import * as Dialog from "@radix-ui/react-dialog" import * as Dialog from '@radix-ui/react-dialog'
import AXSelect from "~components/AxSelect" import AXSelect from '~components/AxSelect'
import ElementToggle from "~components/ElementToggle" import ElementToggle from '~components/ElementToggle'
import WeaponKeyDropdown from "~components/WeaponKeyDropdown" import WeaponKeyDropdown from '~components/WeaponKeyDropdown'
import Button from "~components/Button" import Button from '~components/Button'
import api from "~utils/api" import api from '~utils/api'
import { appState } from "~utils/appState" import { appState } from '~utils/appState'
import CrossIcon from "~public/icons/Cross.svg" import CrossIcon from '~public/icons/Cross.svg'
import "./index.scss" import './index.scss'
interface GridWeaponObject { interface GridWeaponObject {
weapon: { weapon: {
@ -38,11 +38,11 @@ interface Props {
const WeaponModal = (props: Props) => { const WeaponModal = (props: Props) => {
const router = useRouter() const router = useRouter()
const locale = const locale =
router.locale && ["en", "ja"].includes(router.locale) ? router.locale : "en" router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
const { t } = useTranslation("common") const { t } = useTranslation('common')
// Cookies // Cookies
const cookie = getCookie("account") const cookie = getCookie('account')
const accountData: AccountCookie = cookie const accountData: AccountCookie = cookie
? JSON.parse(cookie as string) ? JSON.parse(cookie as string)
: null : null
@ -134,7 +134,7 @@ const WeaponModal = (props: Props) => {
const elementSelect = () => { const elementSelect = () => {
return ( return (
<section> <section>
<h3>{t("modals.weapon.subtitles.element")}</h3> <h3>{t('modals.weapon.subtitles.element')}</h3>
<ElementToggle <ElementToggle
currentElement={props.gridWeapon.element} currentElement={props.gridWeapon.element}
sendValue={receiveElementValue} sendValue={receiveElementValue}
@ -146,7 +146,7 @@ const WeaponModal = (props: Props) => {
const keySelect = () => { const keySelect = () => {
return ( return (
<section> <section>
<h3>{t("modals.weapon.subtitles.weapon_keys")}</h3> <h3>{t('modals.weapon.subtitles.weapon_keys')}</h3>
{[2, 3, 17, 22].includes(props.gridWeapon.object.series) ? ( {[2, 3, 17, 22].includes(props.gridWeapon.object.series) ? (
<WeaponKeyDropdown <WeaponKeyDropdown
currentValue={ currentValue={
@ -159,7 +159,7 @@ const WeaponModal = (props: Props) => {
ref={weaponKey1Select} ref={weaponKey1Select}
/> />
) : ( ) : (
"" ''
)} )}
{[2, 3, 17].includes(props.gridWeapon.object.series) ? ( {[2, 3, 17].includes(props.gridWeapon.object.series) ? (
@ -174,7 +174,7 @@ const WeaponModal = (props: Props) => {
ref={weaponKey2Select} ref={weaponKey2Select}
/> />
) : ( ) : (
"" ''
)} )}
{props.gridWeapon.object.series == 17 ? ( {props.gridWeapon.object.series == 17 ? (
@ -189,7 +189,7 @@ const WeaponModal = (props: Props) => {
ref={weaponKey3Select} ref={weaponKey3Select}
/> />
) : ( ) : (
"" ''
)} )}
</section> </section>
) )
@ -198,7 +198,7 @@ const WeaponModal = (props: Props) => {
const axSelect = () => { const axSelect = () => {
return ( return (
<section> <section>
<h3>{t("modals.weapon.subtitles.ax_skills")}</h3> <h3>{t('modals.weapon.subtitles.ax_skills')}</h3>
<AXSelect <AXSelect
axType={props.gridWeapon.object.ax} axType={props.gridWeapon.object.ax}
currentSkills={props.gridWeapon.ax} currentSkills={props.gridWeapon.ax}
@ -225,7 +225,7 @@ const WeaponModal = (props: Props) => {
<div className="DialogHeader"> <div className="DialogHeader">
<div className="DialogTop"> <div className="DialogTop">
<Dialog.Title className="SubTitle"> <Dialog.Title className="SubTitle">
{t("modals.weapon.title")} {t('modals.weapon.title')}
</Dialog.Title> </Dialog.Title>
<Dialog.Title className="DialogTitle"> <Dialog.Title className="DialogTitle">
{props.gridWeapon.object.name[locale]} {props.gridWeapon.object.name[locale]}
@ -239,16 +239,16 @@ const WeaponModal = (props: Props) => {
</div> </div>
<div className="mods"> <div className="mods">
{props.gridWeapon.object.element == 0 ? elementSelect() : ""} {props.gridWeapon.object.element == 0 ? elementSelect() : ''}
{[2, 3, 17, 24].includes(props.gridWeapon.object.series) {[2, 3, 17, 24].includes(props.gridWeapon.object.series)
? keySelect() ? keySelect()
: ""} : ''}
{props.gridWeapon.object.ax > 0 ? axSelect() : ""} {props.gridWeapon.object.ax > 0 ? axSelect() : ''}
<Button <Button
onClick={updateWeapon} onClick={updateWeapon}
disabled={props.gridWeapon.object.ax > 0 && !formValid} disabled={props.gridWeapon.object.ax > 0 && !formValid}
> >
{t("modals.weapon.buttons.confirm")} {t('modals.weapon.buttons.confirm')}
</Button> </Button>
</div> </div>
</Dialog.Content> </Dialog.Content>

Some files were not shown because too many files have changed in this diff Show more