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>
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Content className="About Dialog" onOpenAutoFocus={ (event) => event.preventDefault() }>
<Dialog.Content
className="About Dialog"
onOpenAutoFocus={(event) => event.preventDefault()}
>
<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>
<span>
<CrossIcon />
@ -28,20 +33,27 @@ const AboutModal = () => {
<section>
<Dialog.Description className="DialogDescription">
Granblue.team is a tool to save and share team compositions for <a href="https://game.granbluefantasy.jp">Granblue Fantasy.</a>
Granblue.team is a tool to save and share team compositions for{' '}
<a href="https://game.granbluefantasy.jp">Granblue Fantasy.</a>
</Dialog.Description>
<Dialog.Description className="DialogDescription">
Start adding things to a team and a URL will be created for you to share it wherever you like, no account needed.
Start adding things to a team and a URL will be created for you to
share it wherever you like, no account needed.
</Dialog.Description>
<Dialog.Description className="DialogDescription">
You can make an account to save any teams you find for future reference, or to keep all of your teams together in one place.
You can make an account to save any teams you find for future
reference, or to keep all of your teams together in one place.
</Dialog.Description>
</section>
<section>
<Dialog.Title className="DialogTitle">Credits</Dialog.Title>
<Dialog.Description className="DialogDescription">
Granblue.team was built by <a href="https://twitter.com/jedmund">@jedmund</a> with a lot of help from <a href="https://twitter.com/lalalalinna">@lalalalinna</a> and <a href="https://twitter.com/tarngerine">@tarngerine</a>.
Granblue.team was built by{' '}
<a href="https://twitter.com/jedmund">@jedmund</a> with a lot of
help from{' '}
<a href="https://twitter.com/lalalalinna">@lalalalinna</a> and{' '}
<a href="https://twitter.com/tarngerine">@tarngerine</a>.
</Dialog.Description>
</section>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,62 +1,101 @@
.Button {
align-items: center;
background: transparent;
background: var(--button-bg);
border: none;
border-radius: 6px;
color: $grey-50;
border-radius: $input-corner;
color: var(--button-text);
display: inline-flex;
font-size: $font-button;
font-weight: $normal;
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 {
background: white;
cursor: pointer;
color: $grey-00;
.icon svg {
fill: $grey-00;
background: var(--button-contained-bg-hover);
}
.icon.stroke svg {
fill: none;
stroke: $grey-00;
&.Save:hover .Accessory svg {
fill: #ff4d4d;
stroke: #ff4d4d;
}
&.Active.Save {
color: #ff4d4d;
.Accessory svg {
fill: #ff4d4d;
stroke: #ff4d4d;
}
&:hover {
color: darken(#ff4d4d, 30);
.Accessory svg {
fill: darken(#ff4d4d, 30);
stroke: darken(#ff4d4d, 30);
}
}
}
}
&.medium {
height: $unit * 5.5;
padding: ($unit * 1.5) $unit-2x;
}
&.small {
padding: $unit * 1.5;
}
&.destructive:hover {
background: $error;
color: white;
color: $grey-100;
.icon svg {
fill: white;
.Accessory svg {
fill: $grey-100;
}
}
&.save:hover {
color: #FF4D4D;
color: #ff4d4d;
.icon svg {
fill: #FF4D4D;
stroke: #FF4D4D;
.Accessory svg {
fill: #ff4d4d;
stroke: #ff4d4d;
}
}
&.save.Active {
color: #FF4D4D;
.icon svg {
fill: #FF4D4D;
stroke: #FF4D4D;
}
color: #ff4d4d;
&:hover {
color: darken(#FF4D4D, 30);
color: darken(#ff4d4d, 30);
.icon svg {
fill: darken(#FF4D4D, 30);
stroke: darken(#FF4D4D, 30);
fill: darken(#ff4d4d, 30);
stroke: darken(#ff4d4d, 30);
}
}
}
@ -69,17 +108,34 @@
color: $error;
&:hover {
color: darken($error, 10)
color: darken($error, 10);
}
}
.icon {
margin-top: 2px;
.Accessory {
$dimension: $unit-2x;
display: flex;
svg {
fill: $grey-50;
height: 12px;
width: 12px;
fill: var(--button-text);
height: $dimension;
width: $dimension;
&.stroke {
fill: none;
stroke: var(--button-text);
}
&.Add {
height: 18px;
width: 18px;
}
&.Check {
height: 22px;
width: 22px;
}
}
&.check svg {
@ -88,28 +144,19 @@
width: auto;
}
&.stroke svg {
fill: none;
stroke: $grey-50;
}
&.settings svg {
svg &.settings svg {
height: 13px;
width: 13px;
}
}
&.Active {
background: white;
}
&.btn-blue {
background: $blue;
color: #8b8b8b;
&:hover {
background: #4B9BE5;
color: #233E56;
background: #4b9be5;
color: #233e56;
}
}
@ -143,70 +190,69 @@
&.null {
background: $grey-90;
color: $grey-50;
color: $grey-55;
&:hover {
background: $grey-70;
color: $grey-00;
color: $grey-15;
}
}
&.wind {
background: $wind-bg-light;
color: $wind-text-dark;
background: $wind-bg-20;
color: $wind-text-10;
&:hover {
background: darken($wind-bg-light, 10);
background: darken($wind-bg-20, 10);
}
}
&.fire {
background: $fire-bg-light;
color: $fire-text-dark;
background: $fire-bg-20;
color: $fire-text-10;
&:hover {
background: darken($fire-bg-light, 10);
background: darken($fire-bg-20, 10);
}
}
&.water {
background: $water-bg-light;
color: $water-text-dark;
background: $water-bg-20;
color: $water-text-10;
&:hover {
background: darken($water-bg-light, 10);
background: darken($water-bg-20, 10);
}
}
&.earth {
background: $earth-bg-light;
color: $earth-text-dark;
background: $earth-bg-20;
color: $earth-text-10;
&:hover {
background: darken($earth-bg-light, 10);
background: darken($earth-bg-20, 10);
}
}
&.dark {
background: $dark-bg-light;
color: $dark-text-dark;
background: $dark-bg-10;
color: $dark-text-10;
&:hover {
background: darken($dark-bg-light, 10);
background: darken($dark-bg-10, 10);
}
}
&.light {
background: $light-bg-light;
color: $light-text-dark;
background: $light-bg-20;
color: $light-text-10;
&:hover {
background: darken($light-bg-light, 10);
background: darken($light-bg-20, 10);
}
}
.text {
.Text {
color: inherit;
display: block;
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 Link from 'next/link'
@ -15,127 +15,174 @@ import SettingsIcon from '~public/icons/Settings.svg'
import './index.scss'
import { ButtonType } from '~utils/enums'
import { access } from 'fs'
interface Props {
interface Props
extends React.DetailedHTMLProps<
React.ButtonHTMLAttributes<HTMLButtonElement>,
HTMLButtonElement
> {
accessoryIcon?: React.ReactNode
active?: boolean
disabled?: boolean
classes?: string[],
icon?: string
type?: ButtonType
children?: React.ReactNode
onClick?: (event: React.MouseEvent<HTMLElement>) => void
blended?: boolean
contained?: boolean
size?: 'small' | 'medium' | 'large'
text?: string
}
const Button = (props: Props) => {
// States
const [active, setActive] = useState(false)
const [disabled, setDisabled] = useState(false)
const [pressed, setPressed] = useState(false)
const [buttonType, setButtonType] = useState(ButtonType.Base)
const defaultProps = {
active: false,
blended: false,
contained: false,
size: 'medium',
}
const classes = classNames({
const Button = React.forwardRef<HTMLButtonElement, Props>(function button(
{ accessoryIcon, active, blended, contained, size, text, ...props },
forwardedRef
) {
const classes = classNames(
{
Button: true,
'Active': active,
'btn-pressed': pressed,
'btn-disabled': disabled,
'save': props.icon === 'save',
'destructive': props.type == ButtonType.Destructive
}, props.classes)
useEffect(() => {
if (props.active) setActive(props.active)
if (props.disabled) setDisabled(props.disabled)
if (props.type) setButtonType(props.type)
}, [props.active, props.disabled, props.type])
const addIcon = (
<span className='icon'>
<AddIcon />
</span>
Active: active,
Blended: blended,
Contained: contained,
// 'btn-pressed': pressed,
// 'btn-disabled': disabled,
// save: props.icon === 'save',
// destructive: props.type == ButtonType.Destructive,
},
size,
props.className
)
const 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
const hasAccessory = () => {
if (accessoryIcon) return <span className="Accessory">{accessoryIcon}</span>
}
return icon
const hasText = () => {
if (text) return <span className="Text">{text}</span>
}
function handleMouseDown() {
setPressed(true)
}
function handleMouseUp() {
setPressed(false)
}
return (
<button
className={classes}
disabled={disabled}
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
onClick={props.onClick}>
{ getIcon() }
{ (props.type != ButtonType.IconOnly) ?
<span className='text'>
{ props.children }
</span> : ''
}
<button {...props} className={classes} ref={forwardedRef}>
{hasAccessory()}
{hasText()}
</button>
)
}
// useEffect(() => {
// if (props.type) setButtonType(props.type)
// }, [props.type])
// const addIcon = (
// <span className="icon">
// <AddIcon />
// </span>
// )
// const menuIcon = (
// <span className="icon">
// <MenuIcon />
// </span>
// )
// const linkIcon = (
// <span className="icon stroke">
// <LinkIcon />
// </span>
// )
// const checkIcon = (
// <span className="icon check">
// <CheckIcon />
// </span>
// )
// const crossIcon = (
// <span className="icon">
// <CrossIcon />
// </span>
// )
// const editIcon = (
// <span className="icon">
// <EditIcon />
// </span>
// )
// const saveIcon = (
// <span className="icon stroke">
// <SaveIcon />
// </span>
// )
// const settingsIcon = (
// <span className="icon settings">
// <SettingsIcon />
// </span>
// )
// function getIcon() {
// let icon: React.ReactNode
// switch (props.icon) {
// case 'new':
// icon = addIcon
// break
// case 'menu':
// icon = menuIcon
// break
// case 'link':
// icon = linkIcon
// break
// case 'check':
// icon = checkIcon
// break
// case 'cross':
// icon = crossIcon
// break
// case 'edit':
// icon = editIcon
// break
// case 'save':
// icon = saveIcon
// break
// case 'settings':
// icon = settingsIcon
// break
// }
// return icon
// }
// function handleMouseDown() {
// setPressed(true)
// }
// function handleMouseUp() {
// setPressed(false)
// }
// return (
// <button
// className={classes}
// disabled={disabled}
// onMouseDown={handleMouseDown}
// onMouseUp={handleMouseUp}
// ref={forwardedRef}
// {...props}
// >
// {getIcon()}
// {props.type != ButtonType.IconOnly ? (
// <span className="text">{children}</span>
// ) : (
// ''
// )}
// </button>
// )
})
Button.defaultProps = defaultProps
export default Button

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

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 {
align-items: center;
background: white;
background: var(--bar-bg);
border-radius: 6px;
display: flex;
flex-direction: row;
@ -18,25 +18,38 @@
}
h1 {
color: $grey-20;
color: var(--text-primary);
font-size: $font-regular;
font-weight: $normal;
flex-grow: 1;
text-align: left;
}
select {
background: url('/icons/Arrow.svg'), $grey-90;
background-repeat: no-repeat;
background-position-y: center;
background-position-x: 95%;
background-size: $unit * 1.5;
color: $grey-50;
select,
.SelectTrigger {
// background: url("/icons/Arrow.svg"), $grey-90;
// background-repeat: no-repeat;
// background-position-y: center;
// background-position-x: 95%;
// background-size: $unit * 1.5;
background-color: var(--select-contained-bg);
color: $grey-55;
font-size: $font-small;
margin: 0;
max-width: 200px;
&:hover {
background-color: var(--select-contained-bg-hover);
}
}
.SelectTrigger {
width: 100%;
span {
font-size: $font-small;
}
}
.UserInfo {
align-items: center;
@ -52,11 +65,11 @@
width: $diameter;
&.gran {
background-color: #CEE7FE;
background-color: #cee7fe;
}
&.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 classNames from 'classnames'
import RaidDropdown from '~components/RaidDropdown'
import './index.scss'
import Select from '~components/Select'
import SelectItem from '~components/SelectItem'
interface Props {
children: React.ReactNode
@ -12,13 +14,24 @@ interface Props {
element?: number
raidSlug?: string
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) => {
// Set up translation
const { t } = useTranslation('common')
const [recencyOpen, setRecencyOpen] = useState(false)
const [elementOpen, setElementOpen] = useState(false)
// Set up refs for filter dropdowns
const elementSelect = 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
const classes = classNames({
'FilterBar': true,
'shadow': props.scrolled
FilterBar: true,
shadow: props.scrolled,
})
function elementSelectChanged() {
const elementValue = (elementSelect.current) ? parseInt(elementSelect.current.value) : -1
function openElementSelect() {
setElementOpen(!elementOpen)
}
function openRecencySelect() {
setRecencyOpen(!recencyOpen)
}
function elementSelectChanged(value: string) {
const elementValue = parseInt(value)
props.onFilter({ element: elementValue })
}
function recencySelectChanged() {
const recencyValue = (recencySelect.current) ? parseInt(recencySelect.current.value) : -1
function recencySelectChanged(value: string) {
const recencyValue = parseInt(value)
props.onFilter({ recency: recencyValue })
}
@ -47,31 +68,76 @@ const FilterBar = (props: Props) => {
return (
<div className={classes}>
{props.children}
<select onChange={elementSelectChanged} ref={elementSelect} value={props.element}>
<option data-element="all" key={-1} value={-1}>{t('elements.full.all')}</option>
<option data-element="null" key={0} value={0}>{t('elements.full.null')}</option>
<option data-element="wind" key={1} value={1}>{t('elements.full.wind')}</option>
<option data-element="fire" key={2} value={2}>{t('elements.full.fire')}</option>
<option data-element="water" key={3} value={3}>{t('elements.full.water')}</option>
<option data-element="earth" key={4} value={4}>{t('elements.full.earth')}</option>
<option data-element="dark" key={5} value={5}>{t('elements.full.dark')}</option>
<option data-element="light" key={6} value={6}>{t('elements.full.light')}</option>
</select>
<Select
defaultValue={-1}
trigger={'All elements'}
open={elementOpen}
onChange={elementSelectChanged}
onClick={openElementSelect}
>
<SelectItem data-element="all" key={-1} value={-1}>
{t('elements.full.all')}
</SelectItem>
<SelectItem data-element="null" key={0} value={0}>
{t('elements.full.null')}
</SelectItem>
<SelectItem data-element="wind" key={1} value={1}>
{t('elements.full.wind')}
</SelectItem>
<SelectItem data-element="fire" key={2} value={2}>
{t('elements.full.fire')}
</SelectItem>
<SelectItem data-element="water" key={3} value={3}>
{t('elements.full.water')}
</SelectItem>
<SelectItem data-element="earth" key={4} value={4}>
{t('elements.full.earth')}
</SelectItem>
<SelectItem data-element="dark" key={5} value={5}>
{t('elements.full.dark')}
</SelectItem>
<SelectItem data-element="light" key={6} value={6}>
{t('elements.full.light')}
</SelectItem>
</Select>
<RaidDropdown
currentRaid={props.raidSlug}
defaultRaid="all"
showAllRaidsOption={true}
onChange={raidSelectChanged}
ref={raidSelect}
/>
<select onChange={recencySelectChanged} ref={recencySelect}>
<option key={-1} value={-1}>{t('recency.all_time')}</option>
<option key={86400} value={86400}>{t('recency.last_day')}</option>
<option key={604800} value={604800}>{t('recency.last_week')}</option>
<option key={2629746} value={2629746}>{t('recency.last_month')}</option>
<option key={7889238} value={7889238}>{t('recency.last_3_months')}</option>
<option key={15778476} value={15778476}>{t('recency.last_6_months')}</option>
<option key={31556952} value={31556952}>{t('recency.last_year')}</option>
</select>
<Select
defaultValue={-1}
trigger={'All time'}
open={recencyOpen}
onChange={recencySelectChanged}
onClick={openRecencySelect}
>
<SelectItem key={-1} value={-1}>
{t('recency.all_time')}
</SelectItem>
<SelectItem key={86400} value={86400}>
{t('recency.last_day')}
</SelectItem>
<SelectItem key={604800} value={604800}>
{t('recency.last_week')}
</SelectItem>
<SelectItem key={2629746} value={2629746}>
{t('recency.last_month')}
</SelectItem>
<SelectItem key={7889238} value={7889238}>
{t('recency.last_3_months')}
</SelectItem>
<SelectItem key={15778476} value={15778476}>
{t('recency.last_6_months')}
</SelectItem>
<SelectItem key={31556952} value={31556952}>
{t('recency.last_year')}
</SelectItem>
</Select>
</div>
)
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -9,7 +9,7 @@ interface Props {
const SegmentedControl: React.FC<Props> = ({ elementClass, children }) => {
return (
<div className="SegmentedControlWrapper">
<div className={`SegmentedControl ${(elementClass) ? elementClass : ''}`}>
<div className={`SegmentedControl ${elementClass ? elementClass : ''}`}>
{children}
</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) {
background: $grey-90;
color: $grey-40;
color: $grey-50;
&:hover {
background: $grey-80;
@ -26,7 +26,7 @@
}
.terms {
color: $grey-40;
color: $grey-50;
font-size: $font-small;
line-height: 1.2;
margin-top: $unit;

View file

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

View file

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

View file

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

View file

@ -17,26 +17,39 @@ interface Props {
const SummonHovercard = (props: Props) => {
const router = useRouter()
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 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() {
let imgSrc = ""
let imgSrc = ''
if (props.gridSummon) {
const summon = props.gridSummon.object
const upgradedSummons = [
'2040094000', '2040100000', '2040080000', '2040098000',
'2040090000', '2040084000', '2040003000', '2040056000'
'2040094000',
'2040100000',
'2040080000',
'2040098000',
'2040090000',
'2040084000',
'2040003000',
'2040056000',
]
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'
// Generate the correct source for the summon
@ -48,18 +61,21 @@ const SummonHovercard = (props: Props) => {
return (
<HoverCard.Root>
<HoverCard.Trigger>
{ props.children }
</HoverCard.Trigger>
<HoverCard.Trigger>{props.children}</HoverCard.Trigger>
<HoverCard.Content className="Weapon Hovercard">
<div className="top">
<div className="title">
<h4>{ props.gridSummon.object.name[locale] }</h4>
<img alt={props.gridSummon.object.name[locale]} src={summonImage()} />
<h4>{props.gridSummon.object.name[locale]}</h4>
<img
alt={props.gridSummon.object.name[locale]}
src={summonImage()}
/>
</div>
<div className="subInfo">
<div className="icons">
<WeaponLabelIcon labelType={Element[props.gridSummon.object.element]}/>
<WeaponLabelIcon
labelType={Element[props.gridSummon.object.element]}
/>
</div>
<UncapIndicator
type="summon"
@ -69,7 +85,9 @@ const SummonHovercard = (props: Props) => {
/>
</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.Content>
</HoverCard.Root>
@ -77,4 +95,3 @@ const SummonHovercard = (props: Props) => {
}
export default SummonHovercard

View file

@ -5,8 +5,12 @@
padding: $unit * 1.5;
&:hover {
background: $grey-90;
background: var(--button-contained-bg);
cursor: pointer;
.Info h5 {
color: var(--text-primary);
}
}
img {
@ -21,10 +25,10 @@
display: flex;
flex-direction: column;
flex-grow: 1;
gap: calc($unit / 2);
gap: $unit-half;
h5 {
color: #555;
color: var(--text-secondary);
display: inline-block;
font-size: $font-medium;
font-weight: $medium;
@ -37,11 +41,11 @@
.stars {
display: inline-block;
color: #FFA15E;
color: #ffa15e;
font-size: $font-xlarge;
& > 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 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
return (
<li className="SummonResult" onClick={props.onClick}>
<img alt={summon.name[locale]} src={`${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/summon-grid/${summon.granblue_id}.jpg`} />
<img
alt={summon.name[locale]}
src={`${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/summon-grid/${summon.granblue_id}.jpg`}
/>
<div className="Info">
<h5>{summon.name[locale]}</h5>
<UncapIndicator

View file

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

View file

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

View file

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

View file

@ -1,5 +1,18 @@
.Fieldset textarea {
color: $grey-00;
font-family: system-ui, -apple-system, 'Helvetica Neue', Helvetica, Arial, sans-serif;
$offset: 2px;
background-color: var(--input-bg);
border: $offset solid transparent;
border-radius: $input-corner;
box-sizing: border-box;
color: var(--text-primary);
display: block;
line-height: 21px;
-webkit-font-smoothing: antialiased;
padding: $unit-2x calc($unit-2x - $offset);
&:focus-within {
border: $offset solid $blue;
outline: none;
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -19,7 +19,7 @@ interface Props {
interface KeyNames {
[key: string]: {
[key: string]: string
en: string,
en: string
ja: string
}
}
@ -27,39 +27,56 @@ interface KeyNames {
const WeaponHovercard = (props: Props) => {
const router = useRouter()
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 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 = {
'2': {
en: 'Pendulum',
ja: 'ペンデュラム'
ja: 'ペンデュラム',
},
'3': {
en: 'Teluma',
ja: 'テルマ'
ja: 'テルマ',
},
'17': {
en: 'Gauph Key',
ja: 'ガフスキー'
ja: 'ガフスキー',
},
'22': {
en: 'Emblem',
ja: 'エンブレム'
}
ja: 'エンブレム',
},
}
const tintElement = (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 tintElement =
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 = () => {
if (props.gridWeapon.position == -1)
return "right"
if (props.gridWeapon.position == -1) return 'right'
else if ([6, 7, 8, 9, 10, 11].includes(props.gridWeapon.position))
return "top"
else
return "bottom"
return 'top'
else return 'bottom'
}
const createPrimaryAxSkillString = () => {
@ -67,9 +84,13 @@ const WeaponHovercard = (props: Props) => {
if (props.gridWeapon.ax) {
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 ''
@ -82,11 +103,17 @@ const WeaponHovercard = (props: Props) => {
const primarySimpleAxSkill = props.gridWeapon.ax[0]
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) {
const secondaryAxSkill = primaryAxSkill.secondary.find(skill => skill.id == secondarySimpleAxSkill.modifier)
return `${secondaryAxSkill?.name[locale]} +${secondarySimpleAxSkill.strength}${ (secondaryAxSkill?.suffix) ? secondaryAxSkill.suffix : '' }`
const secondaryAxSkill = primaryAxSkill.secondary.find(
(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 = (
<section className="weaponKeys">
{ (WeaponKeyNames[props.gridWeapon.object.series]) ?
<h5 className={tintElement}>{ WeaponKeyNames[props.gridWeapon.object.series][locale] }{ (locale === 'en') ? 's' : '' }</h5> : ''
}
{WeaponKeyNames[props.gridWeapon.object.series] ? (
<h5 className={tintElement}>
{WeaponKeyNames[props.gridWeapon.object.series][locale]}
{locale === 'en' ? 's' : ''}
</h5>
) : (
''
)}
{ (props.gridWeapon.weapon_keys) ?
Array.from(Array(props.gridWeapon.weapon_keys.length)).map((x, i) => {
{props.gridWeapon.weapon_keys
? Array.from(Array(props.gridWeapon.weapon_keys.length)).map((x, i) => {
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>
</div>
)
}) : '' }
})
: ''}
</section>
)
@ -124,36 +160,65 @@ const WeaponHovercard = (props: Props) => {
<h5 className={tintElement}>{t('modals.weapon.subtitles.ax_skills')}</h5>
<div className="skills">
<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>
</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">
<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>
</div> : ''}
</div>
) : (
''
)}
</div>
</section>
)
return (
<HoverCard.Root>
<HoverCard.Trigger>
{ props.children }
</HoverCard.Trigger>
<HoverCard.Trigger>{props.children}</HoverCard.Trigger>
<HoverCard.Content className="Weapon Hovercard" side={hovercardSide()}>
<div className="top">
<div className="title">
<h4>{ props.gridWeapon.object.name[locale] }</h4>
<img alt={props.gridWeapon.object.name[locale]} src={weaponImage()} />
<h4>{props.gridWeapon.object.name[locale]}</h4>
<img
alt={props.gridWeapon.object.name[locale]}
src={weaponImage()}
/>
</div>
<div className="subInfo">
<div className="icons">
{ (props.gridWeapon.object.element !== 0 || (props.gridWeapon.object.element === 0 && props.gridWeapon.element != null)) ?
<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] } />
{props.gridWeapon.object.element !== 0 ||
(props.gridWeapon.object.element === 0 &&
props.gridWeapon.element != null) ? (
<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>
<UncapIndicator
type="weapon"
@ -164,9 +229,18 @@ const WeaponHovercard = (props: Props) => {
</div>
</div>
{ (props.gridWeapon.object.ax > 0 && props.gridWeapon.ax && 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>
{props.gridWeapon.object.ax > 0 &&
props.gridWeapon.ax &&
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.Content>
</HoverCard.Root>
@ -174,4 +248,3 @@ const WeaponHovercard = (props: Props) => {
}
export default WeaponHovercard

View file

@ -13,49 +13,51 @@ interface Props {
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 [currentKey, setCurrentKey] = useState('')
const pendulumNames = [
{ en: 'Pendulum', jp: '' },
{ en: 'Chain', jp: '' }
{ en: 'Chain', jp: '' },
]
const telumaNames = [ { en: 'Teluma', jp: '' } ]
const emblemNames = [ { en: 'Emblem', jp: '' } ]
const telumaNames = [{ en: 'Teluma', jp: '' }]
const emblemNames = [{ en: 'Emblem', jp: '' }]
const gauphNames = [
{ en: 'Gauph Key', jp: '' },
{ en: 'Ultima Key', jp: '' },
{ en: 'Gate of Omnipotence', jp: '' }
{ en: 'Gate of Omnipotence', jp: '' },
]
useEffect(() => {
if (props.currentValue)
setCurrentKey(props.currentValue.id)
if (props.currentValue) setCurrentKey(props.currentValue.id)
}, [props.currentValue])
useEffect(() => {
const filterParams = {
params: {
series: props.series,
slot: props.slot
}
slot: props.slot,
},
}
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 = []
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)
}
function fetchWeaponKeys() {
api.endpoints.weapon_keys.getAll(filterParams)
.then((response) => {
api.endpoints.weapon_keys.getAll(filterParams).then((response) => {
const keys = response.data.map((k: any) => k.weapon_key)
organizeWeaponKeys(keys)
})
@ -65,53 +67,55 @@ const WeaponKeyDropdown = React.forwardRef<HTMLSelectElement, Props>(function us
}, [props.series, props.slot])
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) => {
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 } = {}
if (props.series == 2 && index == 0)
name = pendulumNames[0]
if (props.series == 2 && index == 0) name = pendulumNames[0]
else if (props.series == 2 && props.slot == 1 && index == 1)
name = pendulumNames[1]
else if (props.series == 3)
name = telumaNames[index]
else if (props.series == 17)
name = gauphNames[props.slot]
else if (props.series == 22)
name = emblemNames[index]
else if (props.series == 3) name = telumaNames[index]
else if (props.series == 17) name = gauphNames[props.slot]
else if (props.series == 22) name = emblemNames[index]
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}
</optgroup>
)
}
function handleChange(event: React.ChangeEvent<HTMLSelectElement>) {
if (props.onChange)
props.onChange(event)
if (props.onChange) props.onChange(event)
setCurrentKey(event.currentTarget.value)
}
const emptyOption = () => {
let name = ''
if (props.series == 2)
name = pendulumNames[0].en
else if (props.series == 3)
name = telumaNames[0].en
else if (props.series == 17)
name = gauphNames[props.slot].en
else if (props.series == 22)
name = emblemNames[0].en
if (props.series == 2) name = pendulumNames[0].en
else if (props.series == 3) name = telumaNames[0].en
else if (props.series == 17) name = gauphNames[props.slot].en
else if (props.series == 22) name = emblemNames[0].en
return `No ${name}`
}
@ -122,13 +126,17 @@ const WeaponKeyDropdown = React.forwardRef<HTMLSelectElement, Props>(function us
value={currentKey}
onBlur={props.onBlur}
onChange={handleChange}
ref={ref}>
<option key="-1" value="-1">{ emptyOption() }</option>
{ Array.from(Array(keys?.length)).map((x, i) => {
ref={ref}
>
<option key="-1" value="-1">
{emptyOption()}
</option>
{Array.from(Array(keys?.length)).map((x, i) => {
return weaponKeyGroup(i)
})}
</select>
)
})
}
)
export default WeaponKeyDropdown

View file

@ -7,141 +7,140 @@
/* Elements */
&.fire.en {
background-image: url('/labels/element/fire_en.png')
background-image: url('/labels/element/fire_en.png');
}
&.fire.ja {
background-image: url('/labels/element/fire_ja.png')
background-image: url('/labels/element/fire_ja.png');
}
&.water.en {
background-image: url('/labels/element/water_en.png')
background-image: url('/labels/element/water_en.png');
}
&.water.ja {
background-image: url('/labels/element/water_ja.png')
background-image: url('/labels/element/water_ja.png');
}
&.earth.en {
background-image: url('/labels/element/earth_en.png')
background-image: url('/labels/element/earth_en.png');
}
&.earth.ja {
background-image: url('/labels/element/earth_ja.png')
background-image: url('/labels/element/earth_ja.png');
}
&.wind.en {
background-image: url('/labels/element/wind_en.png')
background-image: url('/labels/element/wind_en.png');
}
&.wind.ja {
background-image: url('/labels/element/wind_ja.png')
background-image: url('/labels/element/wind_ja.png');
}
&.dark.en {
background-image: url('/labels/element/dark_en.png')
background-image: url('/labels/element/dark_en.png');
}
&.dark.ja {
background-image: url('/labels/element/dark_ja.png')
background-image: url('/labels/element/dark_ja.png');
}
&.light.en {
background-image: url('/labels/element/light_en.png')
background-image: url('/labels/element/light_en.png');
}
&.light.ja {
background-image: url('/labels/element/light_ja.png')
background-image: url('/labels/element/light_ja.png');
}
&.null.en {
background-image: url('/labels/element/any_en.png')
background-image: url('/labels/element/any_en.png');
}
&.null.ja {
background-image: url('/labels/element/any_ja.png')
background-image: url('/labels/element/any_ja.png');
}
/* Proficiencies */
&.sword.en {
background-image: url('/labels/proficiency/sabre_en.png')
background-image: url('/labels/proficiency/sabre_en.png');
}
&.sword.ja {
background-image: url('/labels/proficiency/sabre_ja.png')
background-image: url('/labels/proficiency/sabre_ja.png');
}
&.dagger.en {
background-image: url('/labels/proficiency/dagger_en.png')
background-image: url('/labels/proficiency/dagger_en.png');
}
&.dagger.ja {
background-image: url('/labels/proficiency/dagger_ja.png')
background-image: url('/labels/proficiency/dagger_ja.png');
}
&.axe.en {
background-image: url('/labels/proficiency/axe_en.png')
background-image: url('/labels/proficiency/axe_en.png');
}
&.axe.ja {
background-image: url('/labels/proficiency/axe_ja.png')
background-image: url('/labels/proficiency/axe_ja.png');
}
&.spear.en {
background-image: url('/labels/proficiency/spear_en.png')
background-image: url('/labels/proficiency/spear_en.png');
}
&.spear.ja {
background-image: url('/labels/proficiency/spear_ja.png')
background-image: url('/labels/proficiency/spear_ja.png');
}
&.staff.en {
background-image: url('/labels/proficiency/staff_en.png')
background-image: url('/labels/proficiency/staff_en.png');
}
&.staff.ja {
background-image: url('/labels/proficiency/staff_ja.png')
background-image: url('/labels/proficiency/staff_ja.png');
}
&.fist.en {
background-image: url('/labels/proficiency/melee_en.png')
background-image: url('/labels/proficiency/melee_en.png');
}
&.fist.ja {
background-image: url('/labels/proficiency/melee_ja.png')
background-image: url('/labels/proficiency/melee_ja.png');
}
&.harp.en {
background-image: url('/labels/proficiency/harp_en.png')
background-image: url('/labels/proficiency/harp_en.png');
}
&.harp.ja {
background-image: url('/labels/proficiency/harp_ja.png')
background-image: url('/labels/proficiency/harp_ja.png');
}
&.gun.en {
background-image: url('/labels/proficiency/gun_en.png')
background-image: url('/labels/proficiency/gun_en.png');
}
&.gun.ja {
background-image: url('/labels/proficiency/gun_ja.png')
background-image: url('/labels/proficiency/gun_ja.png');
}
&.bow.en {
background-image: url('/labels/proficiency/bow_en.png')
background-image: url('/labels/proficiency/bow_en.png');
}
&.bow.ja {
background-image: url('/labels/proficiency/bow_ja.png')
background-image: url('/labels/proficiency/bow_ja.png');
}
&.katana.en {
background-image: url('/labels/proficiency/katana_en.png')
background-image: url('/labels/proficiency/katana_en.png');
}
&.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 router = useRouter()
return (
<i className={`WeaponLabelIcon ${props.labelType} ${router.locale}`} />
)
return <i className={`WeaponLabelIcon ${props.labelType} ${router.locale}`} />
}
export default WeaponLabelIcon

View file

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

View file

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

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